@romansmirnov/doku 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/doku.js +32 -0
- package/bin/init.js +71 -0
- package/client/src/lib/utils.js +4 -0
- package/dist/assets/index-Bq96uZdY.css +1 -0
- package/dist/assets/index-DP3uLjsR.js +196 -0
- package/dist/assets/inter-v12-latin-100-46Mq0mOp.woff +0 -0
- package/dist/assets/inter-v12-latin-100-BQDzDElq.woff2 +0 -0
- package/dist/assets/inter-v12-latin-200-BxfrU12A.woff2 +0 -0
- package/dist/assets/inter-v12-latin-200-DXfqWPZg.woff +0 -0
- package/dist/assets/inter-v12-latin-300-DEbyFmpd.woff2 +0 -0
- package/dist/assets/inter-v12-latin-300-f7r92Nkj.woff +0 -0
- package/dist/assets/inter-v12-latin-500-BQ2gQN_M.woff +0 -0
- package/dist/assets/inter-v12-latin-500-DfX5FI9E.woff2 +0 -0
- package/dist/assets/inter-v12-latin-600-BvOeHRLc.woff2 +0 -0
- package/dist/assets/inter-v12-latin-600-D01NXWOK.woff +0 -0
- package/dist/assets/inter-v12-latin-700-B5TOIllR.woff +0 -0
- package/dist/assets/inter-v12-latin-700-Bj1B9WKG.woff2 +0 -0
- package/dist/assets/inter-v12-latin-800-Bdy4lAMa.woff2 +0 -0
- package/dist/assets/inter-v12-latin-800-DFVvDWwT.woff +0 -0
- package/dist/assets/inter-v12-latin-900-CMga-52B.woff2 +0 -0
- package/dist/assets/inter-v12-latin-900-ORHAl5ZU.woff +0 -0
- package/dist/assets/inter-v12-latin-regular-CahmJf_6.woff +0 -0
- package/dist/assets/inter-v12-latin-regular-YtgfLPRn.woff2 +0 -0
- package/dist/assets/module-CFHRyJc-.js +716 -0
- package/dist/assets/native-CUze18dq.js +1 -0
- package/dist/index.html +13 -0
- package/icons/LICENSE +21 -0
- package/icons/README.md +31 -0
- package/icons/icons/a-arrow-down.tsx +119 -0
- package/icons/icons/a-arrow-up.tsx +120 -0
- package/icons/icons/accessibility.tsx +172 -0
- package/icons/icons/activity.tsx +109 -0
- package/icons/icons/air-vent.tsx +119 -0
- package/icons/icons/airplane.tsx +134 -0
- package/icons/icons/airplay.tsx +130 -0
- package/icons/icons/alarm-clock.tsx +178 -0
- package/icons/icons/align-center.tsx +98 -0
- package/icons/icons/align-horizontal.tsx +126 -0
- package/icons/icons/align-left.tsx +126 -0
- package/icons/icons/align-right.tsx +126 -0
- package/icons/icons/align-vertical.tsx +126 -0
- package/icons/icons/ambulance.tsx +205 -0
- package/icons/icons/angry.tsx +165 -0
- package/icons/icons/annoyed.tsx +163 -0
- package/icons/icons/archive.tsx +132 -0
- package/icons/icons/arrow-big-down-dash.tsx +107 -0
- package/icons/icons/arrow-big-down.tsx +97 -0
- package/icons/icons/arrow-big-left-dash.tsx +107 -0
- package/icons/icons/arrow-big-left.tsx +97 -0
- package/icons/icons/arrow-big-right-dash.tsx +107 -0
- package/icons/icons/arrow-big-right.tsx +97 -0
- package/icons/icons/arrow-big-up-dash.tsx +107 -0
- package/icons/icons/arrow-big-up.tsx +96 -0
- package/icons/icons/arrow-down-0-1.tsx +120 -0
- package/icons/icons/arrow-down-1-0.tsx +120 -0
- package/icons/icons/arrow-down-a-z.tsx +116 -0
- package/icons/icons/arrow-down-left.tsx +113 -0
- package/icons/icons/arrow-down-right.tsx +117 -0
- package/icons/icons/arrow-down-z-a.tsx +116 -0
- package/icons/icons/arrow-down.tsx +112 -0
- package/icons/icons/arrow-left.tsx +112 -0
- package/icons/icons/arrow-right.tsx +112 -0
- package/icons/icons/arrow-up-left.tsx +98 -0
- package/icons/icons/arrow-up-right.tsx +99 -0
- package/icons/icons/arrow-up.tsx +112 -0
- package/icons/icons/at-sign.tsx +135 -0
- package/icons/icons/atom.tsx +124 -0
- package/icons/icons/attach-file.tsx +100 -0
- package/icons/icons/audio-lines.tsx +138 -0
- package/icons/icons/axe.tsx +104 -0
- package/icons/icons/badge-alert.tsx +98 -0
- package/icons/icons/badge-percent.tsx +111 -0
- package/icons/icons/ban.tsx +137 -0
- package/icons/icons/banana.tsx +115 -0
- package/icons/icons/battery-charging.tsx +101 -0
- package/icons/icons/battery-full.tsx +136 -0
- package/icons/icons/battery-low.tsx +116 -0
- package/icons/icons/battery-medium.tsx +127 -0
- package/icons/icons/battery-plus.tsx +102 -0
- package/icons/icons/battery-warning.tsx +104 -0
- package/icons/icons/battery.tsx +100 -0
- package/icons/icons/bell-electric.tsx +124 -0
- package/icons/icons/bell.tsx +93 -0
- package/icons/icons/blocks.tsx +92 -0
- package/icons/icons/bluetooth-connected.tsx +136 -0
- package/icons/icons/bluetooth-off.tsx +124 -0
- package/icons/icons/bluetooth-searching.tsx +128 -0
- package/icons/icons/bluetooth.tsx +99 -0
- package/icons/icons/bold.tsx +95 -0
- package/icons/icons/bone.tsx +98 -0
- package/icons/icons/book-text.tsx +101 -0
- package/icons/icons/bookmark-check.tsx +120 -0
- package/icons/icons/bookmark-minus.tsx +123 -0
- package/icons/icons/bookmark-plus.tsx +137 -0
- package/icons/icons/bookmark-x.tsx +131 -0
- package/icons/icons/bookmark.tsx +98 -0
- package/icons/icons/bot-message-square.tsx +172 -0
- package/icons/icons/bot.tsx +122 -0
- package/icons/icons/box.tsx +117 -0
- package/icons/icons/boxes.tsx +105 -0
- package/icons/icons/brain.tsx +192 -0
- package/icons/icons/calendar-check-2.tsx +111 -0
- package/icons/icons/calendar-check.tsx +111 -0
- package/icons/icons/calendar-cog.tsx +105 -0
- package/icons/icons/calendar-days.tsx +127 -0
- package/icons/icons/cart.tsx +97 -0
- package/icons/icons/cast.tsx +113 -0
- package/icons/icons/cctv.tsx +125 -0
- package/icons/icons/chart-bar-decreasing.tsx +129 -0
- package/icons/icons/chart-bar-increasing.tsx +129 -0
- package/icons/icons/chart-column-decreasing.tsx +130 -0
- package/icons/icons/chart-column-increasing.tsx +130 -0
- package/icons/icons/chart-line.tsx +103 -0
- package/icons/icons/chart-no-axes-column-decreasing.tsx +129 -0
- package/icons/icons/chart-no-axes-column-increasing.tsx +129 -0
- package/icons/icons/chart-pie.tsx +98 -0
- package/icons/icons/chart-scatter.tsx +146 -0
- package/icons/icons/chart-spline.tsx +103 -0
- package/icons/icons/check-check.tsx +117 -0
- package/icons/icons/check.tsx +108 -0
- package/icons/icons/chess-bishop.tsx +112 -0
- package/icons/icons/chess-king.tsx +113 -0
- package/icons/icons/chess-knight.tsx +114 -0
- package/icons/icons/chess-pawn.tsx +150 -0
- package/icons/icons/chevron-down.tsx +94 -0
- package/icons/icons/chevron-first.tsx +102 -0
- package/icons/icons/chevron-left.tsx +94 -0
- package/icons/icons/chevron-right.tsx +95 -0
- package/icons/icons/chevron-up.tsx +94 -0
- package/icons/icons/chevrons-down-up.tsx +108 -0
- package/icons/icons/chevrons-left-right.tsx +108 -0
- package/icons/icons/chevrons-right-left.tsx +108 -0
- package/icons/icons/chevrons-up-down.tsx +108 -0
- package/icons/icons/chrome.tsx +137 -0
- package/icons/icons/circle-check.tsx +107 -0
- package/icons/icons/circle-chevron-down.tsx +99 -0
- package/icons/icons/circle-chevron-left.tsx +97 -0
- package/icons/icons/circle-chevron-right.tsx +97 -0
- package/icons/icons/circle-chevron-up.tsx +99 -0
- package/icons/icons/circle-dashed.tsx +108 -0
- package/icons/icons/circle-dollar-sign.tsx +137 -0
- package/icons/icons/circle-help.tsx +98 -0
- package/icons/icons/clap.tsx +121 -0
- package/icons/icons/clipboard-check.tsx +109 -0
- package/icons/icons/clock.tsx +138 -0
- package/icons/icons/cloud-cog.tsx +101 -0
- package/icons/icons/cloud-download.tsx +98 -0
- package/icons/icons/cloud-lightning.tsx +97 -0
- package/icons/icons/cloud-rain-wind.tsx +109 -0
- package/icons/icons/cloud-rain.tsx +112 -0
- package/icons/icons/cloud-snow.tsx +125 -0
- package/icons/icons/cloud-sun.tsx +137 -0
- package/icons/icons/cloud-upload.tsx +97 -0
- package/icons/icons/coffee.tsx +118 -0
- package/icons/icons/cog.tsx +103 -0
- package/icons/icons/compass.tsx +98 -0
- package/icons/icons/concierge-bell.tsx +123 -0
- package/icons/icons/connect.tsx +164 -0
- package/icons/icons/construction.tsx +111 -0
- package/icons/icons/contrast.tsx +100 -0
- package/icons/icons/cooking-pot.tsx +120 -0
- package/icons/icons/copy.tsx +110 -0
- package/icons/icons/corner-down-left.tsx +98 -0
- package/icons/icons/corner-down-right.tsx +98 -0
- package/icons/icons/corner-left-down.tsx +98 -0
- package/icons/icons/corner-left-up.tsx +98 -0
- package/icons/icons/corner-right-down.tsx +98 -0
- package/icons/icons/corner-right-up.tsx +98 -0
- package/icons/icons/corner-up-left.tsx +98 -0
- package/icons/icons/corner-up-right.tsx +98 -0
- package/icons/icons/cpu.tsx +159 -0
- package/icons/icons/cup-soda.tsx +180 -0
- package/icons/icons/cursor-click.tsx +146 -0
- package/icons/icons/database-backup.tsx +96 -0
- package/icons/icons/delete.tsx +133 -0
- package/icons/icons/disc-3.tsx +93 -0
- package/icons/icons/discord.tsx +131 -0
- package/icons/icons/dollar-sign.tsx +135 -0
- package/icons/icons/download.tsx +99 -0
- package/icons/icons/downvote.tsx +96 -0
- package/icons/icons/dribbble.tsx +183 -0
- package/icons/icons/droplet.tsx +97 -0
- package/icons/icons/drum.tsx +112 -0
- package/icons/icons/earth.tsx +157 -0
- package/icons/icons/euro.tsx +141 -0
- package/icons/icons/ev-charger.tsx +102 -0
- package/icons/icons/expand.tsx +123 -0
- package/icons/icons/eye-off.tsx +99 -0
- package/icons/icons/eye.tsx +100 -0
- package/icons/icons/facebook.tsx +108 -0
- package/icons/icons/feather.tsx +103 -0
- package/icons/icons/figma.tsx +122 -0
- package/icons/icons/file-chart-line.tsx +105 -0
- package/icons/icons/file-check-2.tsx +108 -0
- package/icons/icons/file-check.tsx +108 -0
- package/icons/icons/file-cog.tsx +99 -0
- package/icons/icons/file-pen-line.tsx +115 -0
- package/icons/icons/file-stack.tsx +105 -0
- package/icons/icons/file-text.tsx +159 -0
- package/icons/icons/fingerprint.tsx +197 -0
- package/icons/icons/fish-symbol.tsx +101 -0
- package/icons/icons/flame.tsx +105 -0
- package/icons/icons/flask.tsx +120 -0
- package/icons/icons/folder-archive.tsx +95 -0
- package/icons/icons/folder-check.tsx +106 -0
- package/icons/icons/folder-clock.tsx +139 -0
- package/icons/icons/folder-code.tsx +108 -0
- package/icons/icons/folder-cog.tsx +101 -0
- package/icons/icons/folder-dot.tsx +97 -0
- package/icons/icons/folder-down.tsx +100 -0
- package/icons/icons/folder-git-2.tsx +146 -0
- package/icons/icons/folder-git.tsx +116 -0
- package/icons/icons/folder-heart.tsx +101 -0
- package/icons/icons/folder-input.tsx +100 -0
- package/icons/icons/folder-kanban.tsx +111 -0
- package/icons/icons/folder-key.tsx +104 -0
- package/icons/icons/folder-lock.tsx +103 -0
- package/icons/icons/folder-minus.tsx +96 -0
- package/icons/icons/folder-open.tsx +100 -0
- package/icons/icons/folder-output.tsx +101 -0
- package/icons/icons/folder-plus.tsx +109 -0
- package/icons/icons/folder-root.tsx +107 -0
- package/icons/icons/folder-sync.tsx +102 -0
- package/icons/icons/folder-tree.tsx +145 -0
- package/icons/icons/folder-up.tsx +100 -0
- package/icons/icons/folder-x.tsx +105 -0
- package/icons/icons/folders.tsx +122 -0
- package/icons/icons/frame.tsx +152 -0
- package/icons/icons/frown.tsx +174 -0
- package/icons/icons/gallery-horizontal-end.tsx +117 -0
- package/icons/icons/gallery-thumbnails.tsx +100 -0
- package/icons/icons/gallery-vertical-end.tsx +117 -0
- package/icons/icons/gauge.tsx +102 -0
- package/icons/icons/gavel.tsx +107 -0
- package/icons/icons/georgian-lari.tsx +148 -0
- package/icons/icons/git-branch.tsx +174 -0
- package/icons/icons/git-commit-horizontal.tsx +123 -0
- package/icons/icons/git-commit-vertical.tsx +117 -0
- package/icons/icons/git-compare-arrows.tsx +199 -0
- package/icons/icons/git-compare.tsx +170 -0
- package/icons/icons/git-fork.tsx +189 -0
- package/icons/icons/git-graph.tsx +212 -0
- package/icons/icons/git-merge.tsx +148 -0
- package/icons/icons/git-pull-request-closed.tsx +199 -0
- package/icons/icons/git-pull-request-create.tsx +189 -0
- package/icons/icons/git-pull-request.tsx +166 -0
- package/icons/icons/github.tsx +149 -0
- package/icons/icons/gitlab.tsx +103 -0
- package/icons/icons/graduation-cap.tsx +125 -0
- package/icons/icons/grip-horizontal.tsx +131 -0
- package/icons/icons/grip-vertical.tsx +128 -0
- package/icons/icons/grip.tsx +130 -0
- package/icons/icons/hammer.tsx +105 -0
- package/icons/icons/hand-coins.tsx +150 -0
- package/icons/icons/hand-fist.tsx +98 -0
- package/icons/icons/hand-grab.tsx +96 -0
- package/icons/icons/hand-heart.tsx +114 -0
- package/icons/icons/hand-helping.tsx +95 -0
- package/icons/icons/hand-metal.tsx +96 -0
- package/icons/icons/hand.tsx +95 -0
- package/icons/icons/hard-drive-download.tsx +102 -0
- package/icons/icons/hard-drive-upload.tsx +102 -0
- package/icons/icons/heart-handshake.tsx +97 -0
- package/icons/icons/heart-pulse.tsx +136 -0
- package/icons/icons/heart.tsx +90 -0
- package/icons/icons/history.tsx +159 -0
- package/icons/icons/home.tsx +103 -0
- package/icons/icons/hourglass.tsx +104 -0
- package/icons/icons/id-card.tsx +123 -0
- package/icons/icons/index.ts +3686 -0
- package/icons/icons/indian-rupee.tsx +153 -0
- package/icons/icons/instagram.tsx +187 -0
- package/icons/icons/italic.tsx +126 -0
- package/icons/icons/japanese-yen.tsx +141 -0
- package/icons/icons/key-circle.tsx +93 -0
- package/icons/icons/key-square.tsx +94 -0
- package/icons/icons/key.tsx +104 -0
- package/icons/icons/keyboard.tsx +134 -0
- package/icons/icons/languages.tsx +158 -0
- package/icons/icons/laptop-minimal-check.tsx +109 -0
- package/icons/icons/laugh.tsx +160 -0
- package/icons/icons/layers.tsx +113 -0
- package/icons/icons/layout-grid.tsx +157 -0
- package/icons/icons/layout-panel-top.tsx +143 -0
- package/icons/icons/link.tsx +107 -0
- package/icons/icons/linkedin.tsx +185 -0
- package/icons/icons/loader-circle.tsx +107 -0
- package/icons/icons/loader-pinwheel.tsx +110 -0
- package/icons/icons/loader.tsx +113 -0
- package/icons/icons/lock-keyhole-open.tsx +116 -0
- package/icons/icons/lock-keyhole.tsx +115 -0
- package/icons/icons/lock-open.tsx +114 -0
- package/icons/icons/lock.tsx +114 -0
- package/icons/icons/logout.tsx +105 -0
- package/icons/icons/mail-check.tsx +108 -0
- package/icons/icons/mailbox.tsx +110 -0
- package/icons/icons/map-pin-check-inside.tsx +120 -0
- package/icons/icons/map-pin-check.tsx +120 -0
- package/icons/icons/map-pin-house.tsx +120 -0
- package/icons/icons/map-pin-minus-inside.tsx +120 -0
- package/icons/icons/map-pin-minus.tsx +120 -0
- package/icons/icons/map-pin-off.tsx +122 -0
- package/icons/icons/map-pin-plus-inside.tsx +141 -0
- package/icons/icons/map-pin-plus.tsx +141 -0
- package/icons/icons/map-pin-x-inside.tsx +141 -0
- package/icons/icons/map-pin.tsx +122 -0
- package/icons/icons/maximize-2.tsx +105 -0
- package/icons/icons/maximize.tsx +123 -0
- package/icons/icons/meh.tsx +174 -0
- package/icons/icons/menu.tsx +125 -0
- package/icons/icons/message-circle-check.tsx +107 -0
- package/icons/icons/message-circle-dashed.tsx +108 -0
- package/icons/icons/message-circle-more.tsx +125 -0
- package/icons/icons/message-circle-plus.tsx +131 -0
- package/icons/icons/message-circle-x.tsx +131 -0
- package/icons/icons/message-circle.tsx +107 -0
- package/icons/icons/message-square-check.tsx +108 -0
- package/icons/icons/message-square-dashed.tsx +109 -0
- package/icons/icons/message-square-more.tsx +125 -0
- package/icons/icons/message-square-plus.tsx +131 -0
- package/icons/icons/message-square-x.tsx +131 -0
- package/icons/icons/message-square.tsx +107 -0
- package/icons/icons/mic-off.tsx +112 -0
- package/icons/icons/mic.tsx +104 -0
- package/icons/icons/minimize.tsx +123 -0
- package/icons/icons/monitor-check.tsx +110 -0
- package/icons/icons/moon.tsx +98 -0
- package/icons/icons/nfc.tsx +134 -0
- package/icons/icons/panel-left-close.tsx +99 -0
- package/icons/icons/panel-left-open.tsx +99 -0
- package/icons/icons/panel-right-open.tsx +99 -0
- package/icons/icons/party-popper.tsx +171 -0
- package/icons/icons/pause.tsx +130 -0
- package/icons/icons/pen-tool.tsx +114 -0
- package/icons/icons/philippine-peso.tsx +142 -0
- package/icons/icons/pickaxe.tsx +106 -0
- package/icons/icons/play.tsx +102 -0
- package/icons/icons/plug-zap.tsx +102 -0
- package/icons/icons/plus.tsx +92 -0
- package/icons/icons/pound-sterling.tsx +148 -0
- package/icons/icons/rabbit.tsx +107 -0
- package/icons/icons/radio-tower.tsx +138 -0
- package/icons/icons/radio.tsx +136 -0
- package/icons/icons/redo-dot.tsx +114 -0
- package/icons/icons/redo.tsx +103 -0
- package/icons/icons/refresh-ccw-dot.tsx +88 -0
- package/icons/icons/refresh-ccw.tsx +86 -0
- package/icons/icons/refresh-cw-off.tsx +90 -0
- package/icons/icons/refresh-cw.tsx +83 -0
- package/icons/icons/rocket.tsx +130 -0
- package/icons/icons/rocking-chair.tsx +131 -0
- package/icons/icons/roller-coaster.tsx +109 -0
- package/icons/icons/rotate-ccw.tsx +81 -0
- package/icons/icons/rotate-cw.tsx +81 -0
- package/icons/icons/route.tsx +131 -0
- package/icons/icons/russian-ruble.tsx +136 -0
- package/icons/icons/saudi-riyal.tsx +124 -0
- package/icons/icons/scan-face.tsx +145 -0
- package/icons/icons/scan-text.tsx +139 -0
- package/icons/icons/search.tsx +94 -0
- package/icons/icons/send.tsx +119 -0
- package/icons/icons/settings.tsx +92 -0
- package/icons/icons/shield-check.tsx +109 -0
- package/icons/icons/ship.tsx +120 -0
- package/icons/icons/shower-head.tsx +129 -0
- package/icons/icons/shrink.tsx +123 -0
- package/icons/icons/sliders-horizontal.tsx +247 -0
- package/icons/icons/smartphone-charging.tsx +98 -0
- package/icons/icons/smartphone-nfc.tsx +129 -0
- package/icons/icons/smile-plus.tsx +99 -0
- package/icons/icons/smile.tsx +163 -0
- package/icons/icons/snowflake.tsx +109 -0
- package/icons/icons/sparkles.tsx +148 -0
- package/icons/icons/square-activity.tsx +132 -0
- package/icons/icons/square-arrow-down.tsx +119 -0
- package/icons/icons/square-arrow-left.tsx +119 -0
- package/icons/icons/square-arrow-right.tsx +119 -0
- package/icons/icons/square-arrow-up.tsx +119 -0
- package/icons/icons/square-chevron-down.tsx +96 -0
- package/icons/icons/square-chevron-left.tsx +96 -0
- package/icons/icons/square-chevron-right.tsx +96 -0
- package/icons/icons/square-chevron-up.tsx +96 -0
- package/icons/icons/square-pen.tsx +101 -0
- package/icons/icons/square-stack.tsx +123 -0
- package/icons/icons/stethoscope.tsx +192 -0
- package/icons/icons/sun-dim.tsx +108 -0
- package/icons/icons/sun-medium.tsx +108 -0
- package/icons/icons/sun-moon.tsx +137 -0
- package/icons/icons/sun.tsx +107 -0
- package/icons/icons/sunset.tsx +133 -0
- package/icons/icons/swiss-franc.tsx +141 -0
- package/icons/icons/syringe.tsx +97 -0
- package/icons/icons/telescope.tsx +113 -0
- package/icons/icons/terminal.tsx +103 -0
- package/icons/icons/thermometer.tsx +100 -0
- package/icons/icons/timer.tsx +136 -0
- package/icons/icons/tornado.tsx +138 -0
- package/icons/icons/train-track.tsx +133 -0
- package/icons/icons/trending-down.tsx +151 -0
- package/icons/icons/trending-up-down.tsx +162 -0
- package/icons/icons/trending-up.tsx +150 -0
- package/icons/icons/truck.tsx +186 -0
- package/icons/icons/turkish-lira.tsx +141 -0
- package/icons/icons/twitch.tsx +157 -0
- package/icons/icons/twitter.tsx +109 -0
- package/icons/icons/underline.tsx +108 -0
- package/icons/icons/undo-dot.tsx +114 -0
- package/icons/icons/undo.tsx +109 -0
- package/icons/icons/upload.tsx +99 -0
- package/icons/icons/upvote.tsx +96 -0
- package/icons/icons/user-check.tsx +108 -0
- package/icons/icons/user-round-check.tsx +109 -0
- package/icons/icons/user-round-plus.tsx +127 -0
- package/icons/icons/user.tsx +119 -0
- package/icons/icons/users.tsx +113 -0
- package/icons/icons/vibrate.tsx +106 -0
- package/icons/icons/volume.tsx +135 -0
- package/icons/icons/washing-machine.tsx +132 -0
- package/icons/icons/waves-ladder.tsx +90 -0
- package/icons/icons/waves.tsx +112 -0
- package/icons/icons/waypoints.tsx +146 -0
- package/icons/icons/webhook.tsx +116 -0
- package/icons/icons/wifi-low.tsx +177 -0
- package/icons/icons/wifi.tsx +112 -0
- package/icons/icons/wind-arrow-down.tsx +150 -0
- package/icons/icons/wind.tsx +124 -0
- package/icons/icons/workflow.tsx +127 -0
- package/icons/icons/wrench.tsx +100 -0
- package/icons/icons/x.tsx +102 -0
- package/icons/icons/youtube.tsx +146 -0
- package/icons/icons/zap-off.tsx +115 -0
- package/icons/icons/zap.tsx +105 -0
- package/package.json +52 -0
- package/server/api.js +313 -0
- package/server/config.js +32 -0
- package/server/frontmatter.js +47 -0
- package/server/index.js +34 -0
- package/server/search.js +62 -0
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@romansmirnov/doku",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Personal documentation system with Markdown storage and WYSIWYG editing",
|
|
5
|
+
"main": "server/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"doku": "bin/doku.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"server/",
|
|
12
|
+
"dist/",
|
|
13
|
+
"icons/icons/",
|
|
14
|
+
"client/src/lib/",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"dev": "node server/index.js",
|
|
20
|
+
"build": "vite build",
|
|
21
|
+
"preview": "vite preview",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": ["documentation", "markdown", "wiki", "notes", "knowledge-base"],
|
|
25
|
+
"author": "Roman Smirnov",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"type": "commonjs",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/romasm/Doku.git"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"chokidar": "^5.0.0",
|
|
34
|
+
"cors": "^2.8.6",
|
|
35
|
+
"express": "^5.2.1",
|
|
36
|
+
"glob": "^13.0.6",
|
|
37
|
+
"multer": "^2.1.1"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@blocknote/core": "^0.47.3",
|
|
41
|
+
"@blocknote/mantine": "^0.47.3",
|
|
42
|
+
"@blocknote/react": "^0.47.3",
|
|
43
|
+
"@mantine/core": "^8.3.18",
|
|
44
|
+
"@mantine/hooks": "^8.3.18",
|
|
45
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
46
|
+
"motion": "^12.38.0",
|
|
47
|
+
"react": "^19.2.4",
|
|
48
|
+
"react-dom": "^19.2.4",
|
|
49
|
+
"react-router-dom": "^7.14.0",
|
|
50
|
+
"vite": "^8.0.3"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/server/api.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const multer = require('multer');
|
|
6
|
+
const search = require('./search');
|
|
7
|
+
const config = require('./config');
|
|
8
|
+
const { parseFrontmatter, extractTitle, formatName } = require('./frontmatter');
|
|
9
|
+
|
|
10
|
+
const router = express.Router();
|
|
11
|
+
const DOCS_DIR = config.docsPath;
|
|
12
|
+
|
|
13
|
+
// GET /api/config — public config for frontend
|
|
14
|
+
router.get('/config', (req, res) => {
|
|
15
|
+
res.json({ projectName: config.projectName });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Assets directory for uploaded images
|
|
19
|
+
const ASSETS_DIR = path.join(DOCS_DIR, 'assets');
|
|
20
|
+
if (!fs.existsSync(ASSETS_DIR)) {
|
|
21
|
+
fs.mkdirSync(ASSETS_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Serve assets statically
|
|
25
|
+
router.use('/assets', express.static(ASSETS_DIR));
|
|
26
|
+
|
|
27
|
+
// Image upload
|
|
28
|
+
const upload = multer({
|
|
29
|
+
storage: multer.diskStorage({
|
|
30
|
+
destination: (req, file, cb) => cb(null, ASSETS_DIR),
|
|
31
|
+
filename: (req, file, cb) => {
|
|
32
|
+
const ext = path.extname(file.originalname).toLowerCase();
|
|
33
|
+
const id = crypto.randomBytes(8).toString('hex');
|
|
34
|
+
cb(null, `${id}${ext}`);
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
fileFilter: (req, file, cb) => {
|
|
38
|
+
const allowed = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp'];
|
|
39
|
+
const ext = path.extname(file.originalname).toLowerCase();
|
|
40
|
+
cb(null, allowed.includes(ext));
|
|
41
|
+
},
|
|
42
|
+
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
router.post('/upload', upload.single('image'), (req, res) => {
|
|
46
|
+
if (!req.file) {
|
|
47
|
+
return res.status(400).json({ error: 'No valid image file' });
|
|
48
|
+
}
|
|
49
|
+
const url = `/api/assets/${req.file.filename}`;
|
|
50
|
+
res.json({ url, filename: req.file.filename });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Express 5 returns wildcard params as arrays — join them back into a path string
|
|
54
|
+
function getDocPath(params) {
|
|
55
|
+
const p = params.docPath;
|
|
56
|
+
return Array.isArray(p) ? p.join('/') : p;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Ensure docs directory exists
|
|
60
|
+
if (!fs.existsSync(DOCS_DIR)) {
|
|
61
|
+
fs.mkdirSync(DOCS_DIR, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Read frontmatter + title from a .md file
|
|
65
|
+
function getFileMeta(filePath) {
|
|
66
|
+
try {
|
|
67
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
68
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
69
|
+
const title = extractTitle(body);
|
|
70
|
+
return { ordering: frontmatter.ordering, title };
|
|
71
|
+
} catch {
|
|
72
|
+
return { ordering: undefined, title: null };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Build the file tree. Convention: if name.md and name/ both exist as siblings,
|
|
77
|
+
// name.md is the folder's index file and shown as a folder (not a separate file).
|
|
78
|
+
function buildTree(dirPath, basePath = '') {
|
|
79
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
80
|
+
const items = [];
|
|
81
|
+
|
|
82
|
+
const childFolderNames = new Set(
|
|
83
|
+
entries.filter((e) => e.isDirectory() && !e.name.startsWith('.')).map((e) => e.name)
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (entry.name.startsWith('.') || entry.name === '_meta.json' || entry.name === 'config.json' || entry.name === 'assets') continue;
|
|
88
|
+
|
|
89
|
+
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
90
|
+
|
|
91
|
+
if (entry.isDirectory()) {
|
|
92
|
+
const children = buildTree(path.join(dirPath, entry.name), relativePath);
|
|
93
|
+
|
|
94
|
+
// Read metadata from the sibling index .md file
|
|
95
|
+
const indexFile = path.join(dirPath, entry.name + '.md');
|
|
96
|
+
const meta = fs.existsSync(indexFile) ? getFileMeta(indexFile) : {};
|
|
97
|
+
|
|
98
|
+
items.push({
|
|
99
|
+
name: entry.name,
|
|
100
|
+
title: meta.title || formatName(entry.name),
|
|
101
|
+
path: relativePath,
|
|
102
|
+
type: 'folder',
|
|
103
|
+
ordering: meta.ordering,
|
|
104
|
+
children,
|
|
105
|
+
});
|
|
106
|
+
} else if (entry.name.endsWith('.md')) {
|
|
107
|
+
const nameWithoutExt = entry.name.replace(/\.md$/, '');
|
|
108
|
+
// If a sibling folder with the same name exists, this .md is the folder index — skip it
|
|
109
|
+
if (childFolderNames.has(nameWithoutExt)) continue;
|
|
110
|
+
|
|
111
|
+
const meta = getFileMeta(path.join(dirPath, entry.name));
|
|
112
|
+
|
|
113
|
+
items.push({
|
|
114
|
+
name: nameWithoutExt,
|
|
115
|
+
title: meta.title || formatName(nameWithoutExt),
|
|
116
|
+
path: relativePath.replace(/\.md$/, ''),
|
|
117
|
+
type: 'file',
|
|
118
|
+
ordering: meta.ordering,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Sort by ordering (type doesn't matter — folders and files intermix).
|
|
124
|
+
// Items with ordering come first (by value), then items without (alphabetically).
|
|
125
|
+
items.sort((a, b) => {
|
|
126
|
+
const aHasOrder = a.ordering !== undefined && a.ordering !== null;
|
|
127
|
+
const bHasOrder = b.ordering !== undefined && b.ordering !== null;
|
|
128
|
+
|
|
129
|
+
if (aHasOrder && bHasOrder) return a.ordering - b.ordering;
|
|
130
|
+
if (aHasOrder && !bHasOrder) return -1;
|
|
131
|
+
if (!aHasOrder && bHasOrder) return 1;
|
|
132
|
+
return a.name.localeCompare(b.name);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return items;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Remove empty folder and clean up (used after delete)
|
|
139
|
+
function cleanupEmptyFolder(folderPath) {
|
|
140
|
+
if (!fs.existsSync(folderPath)) return;
|
|
141
|
+
if (!fs.statSync(folderPath).isDirectory()) return;
|
|
142
|
+
|
|
143
|
+
const entries = fs.readdirSync(folderPath);
|
|
144
|
+
if (entries.length === 0) {
|
|
145
|
+
fs.rmdirSync(folderPath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// GET /api/tree — file tree
|
|
150
|
+
router.get('/tree', (req, res) => {
|
|
151
|
+
try {
|
|
152
|
+
const tree = buildTree(DOCS_DIR);
|
|
153
|
+
res.json(tree);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
res.status(500).json({ error: err.message });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// GET /api/doc/* — read a doc
|
|
160
|
+
router.get('/doc/{*docPath}', (req, res) => {
|
|
161
|
+
const docPath = getDocPath(req.params);
|
|
162
|
+
const filePath = path.join(DOCS_DIR, docPath + '.md');
|
|
163
|
+
|
|
164
|
+
if (!filePath.startsWith(DOCS_DIR)) {
|
|
165
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!fs.existsSync(filePath)) {
|
|
169
|
+
return res.status(404).json({ error: 'Not found' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Check if this doc has a matching folder (i.e. it's a folder index)
|
|
173
|
+
const folderPath = path.join(DOCS_DIR, docPath);
|
|
174
|
+
const isFolder = fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory();
|
|
175
|
+
|
|
176
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
177
|
+
const stat = fs.statSync(filePath);
|
|
178
|
+
res.json({ content, path: docPath, updatedAt: stat.mtime, isFolder });
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// PUT /api/doc/* — create or update a doc
|
|
182
|
+
router.put('/doc/{*docPath}', (req, res) => {
|
|
183
|
+
const docPath = getDocPath(req.params);
|
|
184
|
+
const filePath = path.join(DOCS_DIR, docPath + '.md');
|
|
185
|
+
|
|
186
|
+
if (!filePath.startsWith(DOCS_DIR)) {
|
|
187
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const dir = path.dirname(filePath);
|
|
191
|
+
if (!fs.existsSync(dir)) {
|
|
192
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fs.writeFileSync(filePath, req.body.content || '', 'utf-8');
|
|
196
|
+
res.json({ success: true, path: docPath });
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// DELETE /api/doc/* — delete a doc
|
|
200
|
+
router.delete('/doc/{*docPath}', (req, res) => {
|
|
201
|
+
const docPath = getDocPath(req.params);
|
|
202
|
+
const filePath = path.join(DOCS_DIR, docPath + '.md');
|
|
203
|
+
|
|
204
|
+
if (!filePath.startsWith(DOCS_DIR)) {
|
|
205
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!fs.existsSync(filePath)) {
|
|
209
|
+
return res.status(404).json({ error: 'Not found' });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
fs.unlinkSync(filePath);
|
|
213
|
+
|
|
214
|
+
// Auto-cleanup: if the parent folder is now empty, remove it
|
|
215
|
+
// so the sibling .md reverts to a plain doc
|
|
216
|
+
const parentDir = path.dirname(filePath);
|
|
217
|
+
if (parentDir !== DOCS_DIR) {
|
|
218
|
+
cleanupEmptyFolder(parentDir);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
res.json({ success: true });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// POST /api/folder — create a folder (also used to convert a doc into a folder)
|
|
225
|
+
router.post('/folder', (req, res) => {
|
|
226
|
+
const { path: folderPath } = req.body;
|
|
227
|
+
const fullPath = path.join(DOCS_DIR, folderPath);
|
|
228
|
+
|
|
229
|
+
if (!fullPath.startsWith(DOCS_DIR)) {
|
|
230
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!fs.existsSync(fullPath)) {
|
|
234
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// If no sibling index .md exists yet, create one
|
|
238
|
+
const indexFile = fullPath + '.md';
|
|
239
|
+
if (!fs.existsSync(indexFile)) {
|
|
240
|
+
const folderName = path.basename(folderPath);
|
|
241
|
+
const title = formatName(folderName);
|
|
242
|
+
fs.writeFileSync(indexFile, `# ${title}\n\n`, 'utf-8');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
res.json({ success: true, path: folderPath });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// POST /api/move — move/rename a doc or folder
|
|
249
|
+
router.post('/move', (req, res) => {
|
|
250
|
+
const { from, to } = req.body;
|
|
251
|
+
|
|
252
|
+
const fromPath = path.join(DOCS_DIR, from.endsWith('.md') ? from : from + '.md');
|
|
253
|
+
const toPath = path.join(DOCS_DIR, to.endsWith('.md') ? to : to + '.md');
|
|
254
|
+
|
|
255
|
+
if (!fromPath.startsWith(DOCS_DIR) || !toPath.startsWith(DOCS_DIR)) {
|
|
256
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!fs.existsSync(fromPath)) {
|
|
260
|
+
return res.status(404).json({ error: 'Source not found' });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const toDir = path.dirname(toPath);
|
|
264
|
+
if (!fs.existsSync(toDir)) {
|
|
265
|
+
fs.mkdirSync(toDir, { recursive: true });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
fs.renameSync(fromPath, toPath);
|
|
269
|
+
res.json({ success: true });
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// GET /api/folder/* — get folder index content (from sibling .md) + children
|
|
273
|
+
router.get('/folder/{*docPath}', (req, res) => {
|
|
274
|
+
const folderPath = getDocPath(req.params);
|
|
275
|
+
const fullPath = path.join(DOCS_DIR, folderPath);
|
|
276
|
+
|
|
277
|
+
if (!fullPath.startsWith(DOCS_DIR)) {
|
|
278
|
+
return res.status(403).json({ error: 'Forbidden' });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isDirectory()) {
|
|
282
|
+
return res.status(404).json({ error: 'Folder not found' });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Read the sibling index file (e.g. guides.md for guides/)
|
|
286
|
+
const indexFile = fullPath + '.md';
|
|
287
|
+
let content = '';
|
|
288
|
+
if (fs.existsSync(indexFile)) {
|
|
289
|
+
content = fs.readFileSync(indexFile, 'utf-8');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Get direct children (files and folders)
|
|
293
|
+
const children = buildTree(fullPath, folderPath);
|
|
294
|
+
|
|
295
|
+
res.json({ content, path: folderPath, children });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// GET /api/search?q=... — full-text search
|
|
299
|
+
router.get('/search', (req, res) => {
|
|
300
|
+
const query = req.query.q;
|
|
301
|
+
if (!query || query.trim().length === 0) {
|
|
302
|
+
return res.json([]);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const results = search(DOCS_DIR, query.trim());
|
|
307
|
+
res.json(results);
|
|
308
|
+
} catch (err) {
|
|
309
|
+
res.status(500).json({ error: err.message });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
module.exports = router;
|
package/server/config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
// Docs path from CLI argument or default to ./docs (relative to CWD)
|
|
5
|
+
const docsArg = process.argv[2] || './docs';
|
|
6
|
+
const docsPath = path.resolve(process.cwd(), docsArg);
|
|
7
|
+
|
|
8
|
+
// Config lives inside the docs folder
|
|
9
|
+
const configPath = path.join(docsPath, 'config.json');
|
|
10
|
+
|
|
11
|
+
let config = {
|
|
12
|
+
docsPath,
|
|
13
|
+
projectName: 'Doku',
|
|
14
|
+
port: 4782,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
if (fs.existsSync(configPath)) {
|
|
18
|
+
try {
|
|
19
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
20
|
+
if (raw.projectName) config.projectName = raw.projectName;
|
|
21
|
+
if (raw.port) config.port = raw.port;
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore invalid config
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// PORT env var takes highest priority
|
|
28
|
+
if (process.env.PORT) {
|
|
29
|
+
config.port = Number(process.env.PORT);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = config;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const FRONTMATTER_REGEX = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
2
|
+
|
|
3
|
+
function parseFrontmatter(content) {
|
|
4
|
+
const match = content.match(FRONTMATTER_REGEX);
|
|
5
|
+
if (!match) {
|
|
6
|
+
return { frontmatter: {}, body: content };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const raw = match[1];
|
|
10
|
+
const frontmatter = {};
|
|
11
|
+
|
|
12
|
+
for (const line of raw.split('\n')) {
|
|
13
|
+
const idx = line.indexOf(':');
|
|
14
|
+
if (idx === -1) continue;
|
|
15
|
+
const key = line.slice(0, idx).trim();
|
|
16
|
+
let value = line.slice(idx + 1).trim();
|
|
17
|
+
// Parse numbers
|
|
18
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
19
|
+
value = Number(value);
|
|
20
|
+
}
|
|
21
|
+
frontmatter[key] = value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const body = content.slice(match[0].length);
|
|
25
|
+
return { frontmatter, body };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function serializeFrontmatter(frontmatter, body) {
|
|
29
|
+
const keys = Object.keys(frontmatter);
|
|
30
|
+
if (keys.length === 0) return body;
|
|
31
|
+
|
|
32
|
+
const lines = keys.map((k) => `${k}: ${frontmatter[k]}`);
|
|
33
|
+
return `---\n${lines.join('\n')}\n---\n${body}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function extractTitle(body) {
|
|
37
|
+
const match = body.match(/^#\s+(.+)$/m);
|
|
38
|
+
return match ? match[1].trim() : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatName(slug) {
|
|
42
|
+
return slug
|
|
43
|
+
.replace(/-/g, ' ')
|
|
44
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { parseFrontmatter, serializeFrontmatter, extractTitle, formatName };
|
package/server/index.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const cors = require('cors');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const config = require('./config');
|
|
5
|
+
const api = require('./api');
|
|
6
|
+
|
|
7
|
+
const app = express();
|
|
8
|
+
const PORT = config.port;
|
|
9
|
+
|
|
10
|
+
app.use(cors());
|
|
11
|
+
app.use(express.json());
|
|
12
|
+
|
|
13
|
+
app.use('/api', api);
|
|
14
|
+
|
|
15
|
+
// Serve static frontend in production
|
|
16
|
+
const distPath = path.join(__dirname, '..', 'dist');
|
|
17
|
+
app.use(express.static(distPath));
|
|
18
|
+
app.get('{*path}', (req, res) => {
|
|
19
|
+
res.sendFile(path.join(distPath, 'index.html'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
app.listen(PORT, () => {
|
|
23
|
+
console.log(`Server running at http://localhost:${PORT}`);
|
|
24
|
+
}).on('error', (err) => {
|
|
25
|
+
console.error('Server error:', err.message);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
process.on('uncaughtException', (err) => {
|
|
29
|
+
console.error('Uncaught exception:', err);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
process.on('unhandledRejection', (err) => {
|
|
33
|
+
console.error('Unhandled rejection:', err);
|
|
34
|
+
});
|
package/server/search.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { parseFrontmatter, extractTitle, formatName } = require('./frontmatter');
|
|
4
|
+
|
|
5
|
+
function getAllMdFiles(dir, basePath = '') {
|
|
6
|
+
const results = [];
|
|
7
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
8
|
+
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
const fullPath = path.join(dir, entry.name);
|
|
11
|
+
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
12
|
+
|
|
13
|
+
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
14
|
+
results.push(...getAllMdFiles(fullPath, relativePath));
|
|
15
|
+
} else if (entry.name.endsWith('.md')) {
|
|
16
|
+
results.push({ fullPath, relativePath });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return results;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function search(docsDir, query) {
|
|
24
|
+
const files = getAllMdFiles(docsDir);
|
|
25
|
+
const queryLower = query.toLowerCase();
|
|
26
|
+
const results = [];
|
|
27
|
+
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const rawContent = fs.readFileSync(file.fullPath, 'utf-8');
|
|
30
|
+
const { body } = parseFrontmatter(rawContent);
|
|
31
|
+
const bodyLower = body.toLowerCase();
|
|
32
|
+
const idx = bodyLower.indexOf(queryLower);
|
|
33
|
+
|
|
34
|
+
if (idx === -1) {
|
|
35
|
+
const nameLower = file.relativePath.toLowerCase();
|
|
36
|
+
if (!nameLower.includes(queryLower)) continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Extract snippet around first match
|
|
40
|
+
let snippet = '';
|
|
41
|
+
if (idx !== -1) {
|
|
42
|
+
const start = Math.max(0, idx - 60);
|
|
43
|
+
const end = Math.min(body.length, idx + query.length + 60);
|
|
44
|
+
snippet = (start > 0 ? '...' : '') +
|
|
45
|
+
body.slice(start, end).replace(/\n/g, ' ') +
|
|
46
|
+
(end < body.length ? '...' : '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const title = extractTitle(body) ||
|
|
50
|
+
formatName(file.relativePath.replace(/\.md$/, '').split('/').pop());
|
|
51
|
+
|
|
52
|
+
results.push({
|
|
53
|
+
path: file.relativePath.replace(/\.md$/, ''),
|
|
54
|
+
title,
|
|
55
|
+
snippet,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return results.slice(0, 20);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = search;
|