@plebeiantech/lat.md 0.12.3-fork.13 → 0.12.3-fork.15
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/README.md +24 -0
- package/dist/src/cli/check-context.d.ts +56 -0
- package/dist/src/cli/check-context.js +215 -0
- package/dist/src/cli/check-coverage.js +9 -5
- package/dist/src/cli/check-frontmatter.js +1 -1
- package/dist/src/cli/check-mode.js +1 -1
- package/dist/src/cli/check-status.js +1 -1
- package/dist/src/cli/check.d.ts +19 -12
- package/dist/src/cli/check.js +259 -206
- package/dist/src/cli/context.js +1 -1
- package/dist/src/cli/expand.d.ts +1 -2
- package/dist/src/cli/expand.js +52 -5
- package/dist/src/cli/external.d.ts +11 -0
- package/dist/src/cli/external.js +185 -0
- package/dist/src/cli/fork-instructions.d.ts +1 -0
- package/dist/src/cli/fork-instructions.js +3 -2
- package/dist/src/cli/hook.d.ts +26 -0
- package/dist/src/cli/hook.js +112 -40
- package/dist/src/cli/index.js +55 -3
- package/dist/src/cli/init.d.ts +1 -0
- package/dist/src/cli/init.js +40 -2
- package/dist/src/cli/locate.js +4 -3
- package/dist/src/cli/refs.d.ts +2 -1
- package/dist/src/cli/refs.js +88 -43
- package/dist/src/cli/reindex.js +2 -1
- package/dist/src/cli/search.d.ts +12 -3
- package/dist/src/cli/search.js +45 -49
- package/dist/src/cli/section.d.ts +3 -1
- package/dist/src/cli/section.js +114 -38
- package/dist/src/code-refs.d.ts +17 -5
- package/dist/src/code-refs.js +246 -65
- package/dist/src/context.d.ts +3 -0
- package/dist/src/document-formats.d.ts +7 -0
- package/dist/src/document-formats.js +39 -0
- package/dist/src/extensions/alert-marker.d.ts +25 -0
- package/dist/src/extensions/alert-marker.js +55 -0
- package/dist/src/external-documents.d.ts +56 -0
- package/dist/src/external-documents.js +404 -0
- package/dist/src/external-sources.d.ts +136 -0
- package/dist/src/external-sources.js +1098 -0
- package/dist/src/fork/antigravity-hook.d.ts +47 -0
- package/dist/src/fork/antigravity-hook.js +424 -0
- package/dist/src/fork/antigravity-init.d.ts +17 -0
- package/dist/src/fork/antigravity-init.js +124 -0
- package/dist/src/fork/frontmatter-fields.d.ts +36 -0
- package/dist/src/fork/frontmatter-fields.js +23 -0
- package/dist/src/format.d.ts +5 -2
- package/dist/src/format.js +8 -3
- package/dist/src/graph-export.js +1 -1
- package/dist/src/lattice-model.d.ts +77 -0
- package/dist/src/lattice-model.js +464 -0
- package/dist/src/lattice.d.ts +9 -104
- package/dist/src/lattice.js +69 -516
- package/dist/src/markdown-analysis-cache.d.ts +20 -0
- package/dist/src/markdown-analysis-cache.js +139 -0
- package/dist/src/markdown-analysis-worker.d.ts +16 -0
- package/dist/src/markdown-analysis-worker.js +33 -0
- package/dist/src/markdown-analysis.d.ts +50 -0
- package/dist/src/markdown-analysis.js +139 -0
- package/dist/src/markdown-analyzer-loader.d.ts +4 -0
- package/dist/src/markdown-analyzer-loader.js +13 -0
- package/dist/src/markdown-validation.d.ts +25 -0
- package/dist/src/markdown-validation.js +104 -0
- package/dist/src/mcp/server.js +27 -11
- package/dist/src/parser-cache.d.ts +17 -0
- package/dist/src/parser-cache.js +65 -0
- package/dist/src/parser-import.d.ts +8 -0
- package/dist/src/parser-import.js +1 -0
- package/dist/src/parser.js +80 -5
- package/dist/src/path.d.ts +2 -0
- package/dist/src/path.js +4 -0
- package/dist/src/profiler.d.ts +16 -0
- package/dist/src/profiler.js +81 -0
- package/dist/src/project-analysis.d.ts +59 -0
- package/dist/src/project-analysis.js +233 -0
- package/dist/src/project-discovery.d.ts +6 -0
- package/dist/src/project-discovery.js +30 -0
- package/dist/src/repository-path.d.ts +11 -0
- package/dist/src/repository-path.js +52 -0
- package/dist/src/search/db.d.ts +1 -1
- package/dist/src/search/db.js +2 -2
- package/dist/src/search/embedder.d.ts +2 -1
- package/dist/src/search/embedder.js +6 -5
- package/dist/src/search/index.d.ts +2 -1
- package/dist/src/search/index.js +10 -8
- package/dist/src/search/query.d.ts +20 -0
- package/dist/src/search/query.js +67 -0
- package/dist/src/search/search.d.ts +3 -1
- package/dist/src/search/search.js +5 -2
- package/dist/src/source-formats.d.ts +7 -0
- package/dist/src/source-formats.js +26 -0
- package/dist/src/source-parser.d.ts +44 -4
- package/dist/src/source-parser.js +494 -57
- package/dist/src/view/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/src/view/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/src/view/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/src/view/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/src/view/client/assets/MarkdownEditor-B7quqPb8.js +28 -0
- package/dist/src/view/client/assets/OrbitControls-DwcpQNja.js +1 -0
- package/dist/src/view/client/assets/STLLoader-CLWTKt5i.js +1 -0
- package/dist/src/view/client/assets/abnfDiagram-VCTEODGH-V7-T6qsl.js +1 -0
- package/dist/src/view/client/assets/arc-BnwR8Bu7.js +1 -0
- package/dist/src/view/client/assets/architectureDiagram-5GKGNRK7-u97vDna9.js +36 -0
- package/dist/src/view/client/assets/blockDiagram-I7D4REHJ-DETl8e_H.js +129 -0
- package/dist/src/view/client/assets/c4Diagram-7LVT6UL2-DJRAT1Ve.js +38 -0
- package/dist/src/view/client/assets/channel-BwQbnWCN.js +1 -0
- package/dist/src/view/client/assets/chunk-2Q5K7J3B-C0TmOC1Q.js +1 -0
- package/dist/src/view/client/assets/chunk-5VM5RSS4-dfBOrYE6.js +15 -0
- package/dist/src/view/client/assets/chunk-F27PBJKO-DAnks99K.js +1 -0
- package/dist/src/view/client/assets/chunk-IMKFNOWR-CPjB07if.js +231 -0
- package/dist/src/view/client/assets/chunk-JWPE2WC7-VvU_gZI6.js +1 -0
- package/dist/src/view/client/assets/chunk-POPQ4Y6H-DDUbVF55.js +1 -0
- package/dist/src/view/client/assets/chunk-SVP7TREG-BHROkr6D.js +88 -0
- package/dist/src/view/client/assets/chunk-TICWLB2K-95PzGIOg.js +206 -0
- package/dist/src/view/client/assets/chunk-XXDRQBXY-MdmuLhIs.js +1 -0
- package/dist/src/view/client/assets/classDiagram-ZZMXUADV-HvQKkIS-.js +1 -0
- package/dist/src/view/client/assets/classDiagram-v2-VYDZK3BY-HvQKkIS-.js +1 -0
- package/dist/src/view/client/assets/cose-bilkent-JH36ORCC-CauUf_rN.js +1 -0
- package/dist/src/view/client/assets/cynefin-OW5HDTMX-CeA8IKWo.js +166 -0
- package/dist/src/view/client/assets/cynefinDiagram-5FMLGOSQ-L1rtcT8l.js +62 -0
- package/dist/src/view/client/assets/cytoscape.esm-DzSz-X2X.js +321 -0
- package/dist/src/view/client/assets/dagre-GXQ25YYZ-DLQwTa3H.js +4 -0
- package/dist/src/view/client/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/src/view/client/assets/diagram-S7CK7UJ4-kbKdIyXG.js +30 -0
- package/dist/src/view/client/assets/diagram-UQ7AKVKN-kmzp92RC.js +41 -0
- package/dist/src/view/client/assets/diagram-VSXAHHWV-EXJTU8qq.js +3 -0
- package/dist/src/view/client/assets/diagram-VX7I27RA-DAiIyMCG.js +24 -0
- package/dist/src/view/client/assets/diagram-Z3DM3KII-G9wz50ZV.js +24 -0
- package/dist/src/view/client/assets/ebnfDiagram-PWID7BFC-D2tVRCNA.js +1 -0
- package/dist/src/view/client/assets/erDiagram-RLTQ6QDP-C925J5A3.js +99 -0
- package/dist/src/view/client/assets/flowDiagram-HODETNUW-DzoCa-Nq.js +168 -0
- package/dist/src/view/client/assets/ganttDiagram-EL5Y4UJY-D2ebbg4p.js +292 -0
- package/dist/src/view/client/assets/gitGraphDiagram-WWUBYQGX-B4QratYy.js +106 -0
- package/dist/src/view/client/assets/index-BnENo9bV.js +1 -0
- package/dist/src/view/client/assets/index-D0ajr_Ja.css +1 -0
- package/dist/src/view/client/assets/index-KZcnpXJH.js +321 -0
- package/dist/src/view/client/assets/infoDiagram-27XIBGKW-DL4mtHkt.js +2 -0
- package/dist/src/view/client/assets/init-Gi6I4Gst.js +1 -0
- package/dist/src/view/client/assets/ishikawaDiagram-5VMMS53U-CW4mto2d.js +70 -0
- package/dist/src/view/client/assets/journeyDiagram-3NMN7TZE-8R4eacFW.js +139 -0
- package/dist/src/view/client/assets/kanban-definition-UXKFOSKX-Cy9ioXEJ.js +89 -0
- package/dist/src/view/client/assets/katex-HP8lGamR.js +257 -0
- package/dist/src/view/client/assets/layout-C0MFuhth.js +1 -0
- package/dist/src/view/client/assets/linear-Dv3N4dQR.js +1 -0
- package/dist/src/view/client/assets/logo-small-RR_KBOAT.svg +149 -0
- package/dist/src/view/client/assets/maplibre-gl-DnqZgfUS.js +800 -0
- package/dist/src/view/client/assets/mermaid.core-bds9A7e2.js +310 -0
- package/dist/src/view/client/assets/mindmap-definition-YA3MSWOX-DjmGW355.js +96 -0
- package/dist/src/view/client/assets/ordinal-Cboi1Yqb.js +1 -0
- package/dist/src/view/client/assets/pegDiagram-XKGWAZYB-BWsZp0ab.js +1 -0
- package/dist/src/view/client/assets/pieDiagram-E7YTZNPT-CCH2gGR-.js +39 -0
- package/dist/src/view/client/assets/quadrantDiagram-AXDQQJYC-CjocPJFz.js +7 -0
- package/dist/src/view/client/assets/railroadDiagram-O6MQD6OU-BjLeb2KF.js +1 -0
- package/dist/src/view/client/assets/requirementDiagram-BXWQKSXE-Ct6AoXRr.js +84 -0
- package/dist/src/view/client/assets/sankeyDiagram-P5KCCOFB-CIpgGN_w.js +40 -0
- package/dist/src/view/client/assets/sequenceDiagram-WJ2MYXX4-DrmLFj6w.js +162 -0
- package/dist/src/view/client/assets/sizeCapture-INFHLROL-BdFMe2Lr.js +1 -0
- package/dist/src/view/client/assets/stateDiagram-D77RDMKH-Zd9F1goW.js +1 -0
- package/dist/src/view/client/assets/stateDiagram-v2-MP3YSRHH-ByIKCftl.js +1 -0
- package/dist/src/view/client/assets/swimlanes-42K2YHIH-a7VFwi00.js +1 -0
- package/dist/src/view/client/assets/swimlanesDiagram-VR7AAH4N-CzxaUHB-.js +8 -0
- package/dist/src/view/client/assets/three.module-PDSP0dbZ.js +4108 -0
- package/dist/src/view/client/assets/timeline-definition-24CTP7MA-BExPu6X5.js +120 -0
- package/dist/src/view/client/assets/vennDiagram-4TSXK5OY-PC1MU3si.js +34 -0
- package/dist/src/view/client/assets/wardleyDiagram-VM6X3IG4-DNWqU3fa.js +78 -0
- package/dist/src/view/client/assets/xychartDiagram-S5SC5T6Z-E1uA28_D.js +7 -0
- package/dist/src/view/client/index.html +2 -2
- package/dist/src/view/client-shell.d.ts +2 -0
- package/dist/src/view/client-shell.js +5 -0
- package/dist/src/view/diagnostics.d.ts +3 -2
- package/dist/src/view/diagnostics.js +49 -58
- package/dist/src/view/document-edit.d.ts +8 -0
- package/dist/src/view/document-edit.js +25 -0
- package/dist/src/view/document-route.d.ts +12 -0
- package/dist/src/view/document-route.js +93 -0
- package/dist/src/view/document-tree.d.ts +18 -0
- package/dist/src/view/document-tree.js +139 -0
- package/dist/src/view/external-document-tree.d.ts +3 -0
- package/dist/src/view/external-document-tree.js +674 -0
- package/dist/src/view/git-diff.js +187 -0
- package/dist/src/view/git.js +1 -1
- package/dist/src/view/graph.d.ts +3 -2
- package/dist/src/view/graph.js +94 -7
- package/dist/src/view/highlight.d.ts +10 -2
- package/dist/src/view/highlight.js +4540 -1360
- package/dist/src/view/markdown.d.ts +3 -2
- package/dist/src/view/markdown.js +214 -53
- package/dist/src/view/protocol.d.ts +59 -6
- package/dist/src/view/references.d.ts +10 -25
- package/dist/src/view/references.js +117 -107
- package/dist/src/view/repository.d.ts +13 -3
- package/dist/src/view/repository.js +234 -15
- package/dist/src/view/search.d.ts +1 -1
- package/dist/src/view/search.js +3 -7
- package/dist/src/view/server.d.ts +11 -2
- package/dist/src/view/server.js +212 -70
- package/dist/src/view/source-target.js +5 -8
- package/dist/src/view/static-build.d.ts +1 -0
- package/dist/src/view/static-build.js +245 -42
- package/dist/src/view/static-protocol.d.ts +19 -2
- package/dist/src/view/store.d.ts +16 -3
- package/dist/src/view/store.js +228 -36
- package/dist/src/view/table-of-contents.d.ts +2 -2
- package/dist/src/view/table-of-contents.js +2 -22
- package/dist/src/walk.d.ts +5 -3
- package/dist/src/walk.js +102 -8
- package/dist/vendor/lat.md-server/cli.d.ts +2 -0
- package/dist/vendor/lat.md-server/cli.js +12 -0
- package/dist/vendor/lat.md-server/index.d.ts +38 -0
- package/dist/vendor/lat.md-server/index.js +128 -0
- package/package.json +52 -12
- package/plugins/lat-md/README.md +61 -0
- package/plugins/lat-md/hooks.json +38 -0
- package/plugins/lat-md/plugin.json +9 -0
- package/plugins/lat-md/rules/AGENTS.md +118 -0
- package/plugins/lat-md/skills/lat-md/SKILL.md +178 -0
- package/plugins/lat-md/skills/lat-md-conventions/SKILL.md +182 -0
- package/templates/AGENTS.md +15 -3
- package/templates/cursor-rules.md +12 -3
- package/templates/skill/SKILL.md +22 -1
- package/dist/src/view/client/assets/index-C3iaZyYT.css +0 -1
- package/dist/src/view/client/assets/index-KWi67IMz.js +0 -334
- package/dist/src/view/client/assets/logo-CNF8zs47.svg +0 -182
package/dist/src/view/search.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dirname, relative, resolve } from 'node:path';
|
|
2
|
-
import { toPosix } from '../
|
|
2
|
+
import { toPosix } from '../path.js';
|
|
3
|
+
import { documentUrl } from './document-route.js';
|
|
3
4
|
const VIEW_SEARCH_LIMIT = 10;
|
|
4
5
|
const defaultDependencies = {
|
|
5
6
|
async runIndex(latDir) {
|
|
@@ -11,11 +12,6 @@ const defaultDependencies = {
|
|
|
11
12
|
return runSearch(latDir, query, limit, undefined, options);
|
|
12
13
|
},
|
|
13
14
|
};
|
|
14
|
-
function documentUrl(path, headingId) {
|
|
15
|
-
const encodedPath = path.split('/').map(encodeURIComponent).join('/');
|
|
16
|
-
const fragment = headingId ? `#${encodeURIComponent(headingId)}` : '';
|
|
17
|
-
return `/docs/${encodedPath}${fragment}`;
|
|
18
|
-
}
|
|
19
15
|
function viewSearchResult(latDir, match) {
|
|
20
16
|
const section = match.section;
|
|
21
17
|
const projectRoot = dirname(latDir);
|
|
@@ -27,7 +23,7 @@ function viewSearchResult(latDir, match) {
|
|
|
27
23
|
path,
|
|
28
24
|
breadcrumbs: [...fileBreadcrumbs, ...section.id.split('#').slice(1)],
|
|
29
25
|
description: section.firstParagraph,
|
|
30
|
-
url: documentUrl(path, section.githubSlug),
|
|
26
|
+
url: documentUrl(path, section.githubSlug ?? ''),
|
|
31
27
|
score: match.score ?? 0,
|
|
32
28
|
};
|
|
33
29
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Server } from 'node:http';
|
|
2
|
-
import type
|
|
2
|
+
import { type LatServerApp } from '../../vendor/lat.md-server/index.js';
|
|
3
|
+
import { type CmdContext } from '../context.js';
|
|
3
4
|
import { type ViewSearch } from './search.js';
|
|
4
5
|
import { type ViewStore } from './store.js';
|
|
5
6
|
export declare const DEFAULT_VIEW_PORT = 4242;
|
|
@@ -9,6 +10,11 @@ export type ViewServer = {
|
|
|
9
10
|
url: string;
|
|
10
11
|
close: () => Promise<void>;
|
|
11
12
|
};
|
|
13
|
+
export type ViewApp = {
|
|
14
|
+
app: LatServerApp;
|
|
15
|
+
store: ViewStore;
|
|
16
|
+
close: () => Promise<void>;
|
|
17
|
+
};
|
|
12
18
|
export type ViewServerOptions = {
|
|
13
19
|
clientDir?: string;
|
|
14
20
|
git?: boolean;
|
|
@@ -18,6 +24,9 @@ export type ViewServerOptions = {
|
|
|
18
24
|
port?: number;
|
|
19
25
|
search?: ViewSearch;
|
|
20
26
|
watch?: boolean;
|
|
27
|
+
externalCa?: string | Buffer;
|
|
21
28
|
};
|
|
22
|
-
/**
|
|
29
|
+
/** Create the portable Express application used by live Lat UI servers. */
|
|
30
|
+
export declare function createViewApp(ctx: CmdContext, options?: ViewServerOptions): Promise<ViewApp>;
|
|
31
|
+
/** Start the loopback server used by `lat ui`. */
|
|
23
32
|
export declare function startViewServer(ctx: CmdContext, options?: ViewServerOptions): Promise<ViewServer>;
|
package/dist/src/view/server.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { extname, join, relative, resolve, sep } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createLatServerApp, listenLatServer, } from '../../vendor/lat.md-server/index.js';
|
|
6
|
+
import { plainStyler } from '../context.js';
|
|
7
|
+
import { sectionCommand } from '../cli/section.js';
|
|
5
8
|
import { DEFAULT_VIEW_LOGO_TEXT, } from './protocol.js';
|
|
6
|
-
import {
|
|
9
|
+
import { ViewDocumentConflictError } from './document-edit.js';
|
|
10
|
+
import { ViewExternalNotFoundError, ViewDocumentNotFoundError, ViewSourceNotFoundError, } from './repository.js';
|
|
7
11
|
import { createViewSearch } from './search.js';
|
|
8
12
|
import { createViewStore } from './store.js';
|
|
13
|
+
import { rewriteClientAssetUrls } from './client-shell.js';
|
|
14
|
+
import { documentResourcePath, documentUrl, rawDocumentPath, } from './document-route.js';
|
|
9
15
|
const DEFAULT_HOST = '127.0.0.1';
|
|
10
16
|
export const DEFAULT_VIEW_PORT = 4242;
|
|
17
|
+
const MAX_DOCUMENT_EDIT_BODY_BYTES = 16 * 1024 * 1024;
|
|
18
|
+
const MAX_DOCUMENT_CONTENT_LENGTH = 8 * 1024 * 1024;
|
|
11
19
|
const defaultClientDir = fileURLToPath(new URL('./client/', import.meta.url));
|
|
12
|
-
function documentUrl(path) {
|
|
13
|
-
return `/docs/${path.split('/').map(encodeURIComponent).join('/')}`;
|
|
14
|
-
}
|
|
15
|
-
function setSecurityHeaders(res) {
|
|
16
|
-
res.setHeader('Content-Security-Policy', "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'");
|
|
17
|
-
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
18
|
-
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
19
|
-
}
|
|
20
20
|
function send(res, status, contentType, body, headOnly = false) {
|
|
21
21
|
res.statusCode = status;
|
|
22
22
|
res.setHeader('Content-Type', contentType);
|
|
@@ -27,6 +27,24 @@ function sendJson(res, status, value, headOnly) {
|
|
|
27
27
|
res.setHeader('Cache-Control', 'no-store');
|
|
28
28
|
send(res, status, 'application/json; charset=utf-8', JSON.stringify(value), headOnly);
|
|
29
29
|
}
|
|
30
|
+
async function readJsonBody(req) {
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let size = 0;
|
|
33
|
+
for await (const value of req) {
|
|
34
|
+
const chunk = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
35
|
+
size += chunk.byteLength;
|
|
36
|
+
if (size > MAX_DOCUMENT_EDIT_BODY_BYTES) {
|
|
37
|
+
throw new RangeError('Document edit is too large');
|
|
38
|
+
}
|
|
39
|
+
chunks.push(chunk);
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
throw new SyntaxError('Document edit must be valid JSON');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
30
48
|
function contentType(path) {
|
|
31
49
|
switch (extname(path)) {
|
|
32
50
|
case '.css':
|
|
@@ -37,8 +55,17 @@ function contentType(path) {
|
|
|
37
55
|
return 'text/javascript; charset=utf-8';
|
|
38
56
|
case '.json':
|
|
39
57
|
return 'application/json; charset=utf-8';
|
|
58
|
+
case '.gif':
|
|
59
|
+
return 'image/gif';
|
|
60
|
+
case '.jpeg':
|
|
61
|
+
case '.jpg':
|
|
62
|
+
return 'image/jpeg';
|
|
63
|
+
case '.png':
|
|
64
|
+
return 'image/png';
|
|
40
65
|
case '.svg':
|
|
41
66
|
return 'image/svg+xml';
|
|
67
|
+
case '.webp':
|
|
68
|
+
return 'image/webp';
|
|
42
69
|
case '.woff2':
|
|
43
70
|
return 'font/woff2';
|
|
44
71
|
default:
|
|
@@ -71,23 +98,20 @@ async function sendClientFile(res, path, headOnly, immutable = false) {
|
|
|
71
98
|
send(res, 404, 'text/plain; charset=utf-8', 'Not found', headOnly);
|
|
72
99
|
}
|
|
73
100
|
}
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
server.once('listening', onListening);
|
|
86
|
-
server.listen(port, host);
|
|
87
|
-
});
|
|
101
|
+
async function sendClientShell(res, path, headOnly) {
|
|
102
|
+
try {
|
|
103
|
+
const body = rewriteClientAssetUrls(await readFile(path, 'utf8'), '/');
|
|
104
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
105
|
+
send(res, 200, 'text/html; charset=utf-8', body, headOnly);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error.code !== 'ENOENT')
|
|
109
|
+
throw error;
|
|
110
|
+
send(res, 404, 'text/plain; charset=utf-8', 'Not found', headOnly);
|
|
111
|
+
}
|
|
88
112
|
}
|
|
89
|
-
/**
|
|
90
|
-
export async function
|
|
113
|
+
/** Create the portable Express application used by live Lat UI servers. */
|
|
114
|
+
export async function createViewApp(ctx, options = {}) {
|
|
91
115
|
const host = options.host ?? DEFAULT_HOST;
|
|
92
116
|
const clientDir = options.clientDir ?? defaultClientDir;
|
|
93
117
|
const logoText = options.logoText ?? DEFAULT_VIEW_LOGO_TEXT;
|
|
@@ -95,27 +119,34 @@ export async function startViewServer(ctx, options = {}) {
|
|
|
95
119
|
git: options.git,
|
|
96
120
|
gitPollMs: options.gitPollMs,
|
|
97
121
|
watch: options.watch,
|
|
122
|
+
externalCa: options.externalCa,
|
|
98
123
|
});
|
|
99
124
|
const search = options.search ??
|
|
100
125
|
createViewSearch(ctx.latDir, undefined, () => store.markdownGeneration);
|
|
101
126
|
const eventClients = new Set();
|
|
127
|
+
const instanceId = randomUUID();
|
|
102
128
|
const broadcastChange = (change) => {
|
|
103
|
-
const message = `event: change\ndata: ${JSON.stringify(change)}\n\n`;
|
|
129
|
+
const message = `event: change\ndata: ${JSON.stringify({ ...change, instanceId })}\n\n`;
|
|
104
130
|
for (const client of eventClients)
|
|
105
131
|
client.write(message);
|
|
106
132
|
};
|
|
107
133
|
const unsubscribeStore = store.subscribe(broadcastChange);
|
|
108
|
-
const
|
|
134
|
+
const heartbeat = setInterval(() => {
|
|
135
|
+
for (const client of eventClients)
|
|
136
|
+
client.write(': heartbeat\n\n');
|
|
137
|
+
}, 15_000);
|
|
138
|
+
heartbeat.unref();
|
|
139
|
+
const handleRequest = (req, res) => {
|
|
109
140
|
void (async () => {
|
|
110
|
-
setSecurityHeaders(res);
|
|
111
141
|
const method = req.method ?? 'GET';
|
|
112
142
|
const headOnly = method === 'HEAD';
|
|
113
|
-
|
|
143
|
+
const url = new URL(req.url ?? '/', `http://${host}`);
|
|
144
|
+
const documentEdit = method === 'PATCH' && url.pathname === '/api/document';
|
|
145
|
+
if (method !== 'GET' && !headOnly && !documentEdit) {
|
|
114
146
|
res.setHeader('Allow', 'GET, HEAD');
|
|
115
147
|
send(res, 405, 'text/plain; charset=utf-8', 'Method not allowed');
|
|
116
148
|
return;
|
|
117
149
|
}
|
|
118
|
-
const url = new URL(req.url ?? '/', `http://${host}`);
|
|
119
150
|
if (url.pathname === '/') {
|
|
120
151
|
const entry = store.getIndex().entry;
|
|
121
152
|
if (!entry) {
|
|
@@ -146,12 +177,52 @@ export async function startViewServer(ctx, options = {}) {
|
|
|
146
177
|
return;
|
|
147
178
|
}
|
|
148
179
|
eventClients.add(res);
|
|
149
|
-
res.write(`
|
|
180
|
+
res.write(`retry: 1000\nevent: ready\ndata: ${JSON.stringify({ instanceId, generation: store.snapshot.generation, markdownGeneration: store.markdownGeneration })}\n\n`);
|
|
150
181
|
req.once('close', () => eventClients.delete(res));
|
|
151
182
|
return;
|
|
152
183
|
}
|
|
153
184
|
if (url.pathname === '/api/document') {
|
|
154
185
|
const path = url.searchParams.get('path') ?? '';
|
|
186
|
+
if (documentEdit) {
|
|
187
|
+
let body;
|
|
188
|
+
try {
|
|
189
|
+
body = await readJsonBody(req);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
sendJson(res, error instanceof RangeError ? 413 : 400, { error: error.message }, false);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (!body ||
|
|
196
|
+
typeof body !== 'object' ||
|
|
197
|
+
typeof body.baseContent !==
|
|
198
|
+
'string' ||
|
|
199
|
+
typeof body.content !==
|
|
200
|
+
'string') {
|
|
201
|
+
sendJson(res, 400, {
|
|
202
|
+
error: 'Document edit content is required',
|
|
203
|
+
}, false);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const edit = body;
|
|
207
|
+
if (edit.baseContent.length > MAX_DOCUMENT_CONTENT_LENGTH ||
|
|
208
|
+
edit.content.length > MAX_DOCUMENT_CONTENT_LENGTH) {
|
|
209
|
+
sendJson(res, 413, { error: 'Document edit is too large' }, false);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
sendJson(res, 200, await store.editDocument(path, edit.baseContent, edit.content), false);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (error instanceof ViewDocumentConflictError) {
|
|
217
|
+
sendJson(res, 409, { error: error.message }, false);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (!(error instanceof ViewDocumentNotFoundError))
|
|
221
|
+
throw error;
|
|
222
|
+
sendJson(res, 404, { error: error.message }, false);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
155
226
|
try {
|
|
156
227
|
sendJson(res, 200, await store.getDocument(path), headOnly);
|
|
157
228
|
}
|
|
@@ -162,6 +233,18 @@ export async function startViewServer(ctx, options = {}) {
|
|
|
162
233
|
}
|
|
163
234
|
return;
|
|
164
235
|
}
|
|
236
|
+
if (url.pathname === '/api/document-source') {
|
|
237
|
+
const path = url.searchParams.get('path') ?? '';
|
|
238
|
+
try {
|
|
239
|
+
sendJson(res, 200, await store.getDocumentSource(path), headOnly);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (!(error instanceof ViewDocumentNotFoundError))
|
|
243
|
+
throw error;
|
|
244
|
+
sendJson(res, 404, { error: error.message }, headOnly);
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
165
248
|
if (url.pathname === '/api/search') {
|
|
166
249
|
const query = (url.searchParams.get('query') ?? '').trim();
|
|
167
250
|
if (query.length > 500) {
|
|
@@ -171,6 +254,30 @@ export async function startViewServer(ctx, options = {}) {
|
|
|
171
254
|
sendJson(res, 200, await search(query), headOnly);
|
|
172
255
|
return;
|
|
173
256
|
}
|
|
257
|
+
if (url.pathname === '/api/section') {
|
|
258
|
+
const query = (url.searchParams.get('query') ?? '').trim();
|
|
259
|
+
if (!query) {
|
|
260
|
+
sendJson(res, 400, { error: 'Section ID is required' }, headOnly);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (query.length > 1_000) {
|
|
264
|
+
sendJson(res, 400, { error: 'Section ID is too long' }, headOnly);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const result = await sectionCommand({
|
|
268
|
+
latDir: ctx.latDir,
|
|
269
|
+
projectRoot: ctx.projectRoot,
|
|
270
|
+
styler: plainStyler,
|
|
271
|
+
mode: 'cli',
|
|
272
|
+
}, query);
|
|
273
|
+
const tree = await store.renderSectionOutput(result.output, query);
|
|
274
|
+
sendJson(res, 200, {
|
|
275
|
+
output: result.output,
|
|
276
|
+
tree,
|
|
277
|
+
isError: result.isError === true,
|
|
278
|
+
}, headOnly);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
174
281
|
if (url.pathname === '/api/source') {
|
|
175
282
|
const path = url.searchParams.get('path') ?? '';
|
|
176
283
|
const symbol = url.searchParams.get('symbol') ?? '';
|
|
@@ -193,6 +300,18 @@ export async function startViewServer(ctx, options = {}) {
|
|
|
193
300
|
}
|
|
194
301
|
return;
|
|
195
302
|
}
|
|
303
|
+
if (url.pathname === '/api/external') {
|
|
304
|
+
const target = url.searchParams.get('target') ?? '';
|
|
305
|
+
try {
|
|
306
|
+
sendJson(res, 200, await store.getExternal(target), headOnly);
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
if (!(error instanceof ViewExternalNotFoundError))
|
|
310
|
+
throw error;
|
|
311
|
+
sendJson(res, 404, { error: error.message }, headOnly);
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
196
315
|
if (url.pathname.startsWith('/assets/')) {
|
|
197
316
|
const path = clientPath(clientDir, url.pathname);
|
|
198
317
|
if (!path) {
|
|
@@ -202,11 +321,40 @@ export async function startViewServer(ctx, options = {}) {
|
|
|
202
321
|
await sendClientFile(res, path, headOnly, true);
|
|
203
322
|
return;
|
|
204
323
|
}
|
|
324
|
+
const rawMarkdownPath = rawDocumentPath(url.pathname);
|
|
325
|
+
if (rawMarkdownPath) {
|
|
326
|
+
try {
|
|
327
|
+
const source = await store.getDocumentSource(rawMarkdownPath);
|
|
328
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
329
|
+
send(res, 200, 'text/markdown; charset=utf-8', source.content, headOnly);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
if (!(error instanceof ViewDocumentNotFoundError))
|
|
333
|
+
throw error;
|
|
334
|
+
send(res, 404, 'text/plain; charset=utf-8', error.message, headOnly);
|
|
335
|
+
}
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const resourcePath = documentResourcePath(url.pathname);
|
|
339
|
+
if (resourcePath) {
|
|
340
|
+
try {
|
|
341
|
+
const body = await store.getDocumentResource(resourcePath);
|
|
342
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
343
|
+
send(res, 200, contentType(resourcePath), body, headOnly);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
if (!(error instanceof ViewDocumentNotFoundError))
|
|
347
|
+
throw error;
|
|
348
|
+
send(res, 404, 'text/plain; charset=utf-8', error.message, headOnly);
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
205
352
|
if (url.pathname === '/search' ||
|
|
206
353
|
url.pathname === '/graph' ||
|
|
207
354
|
url.pathname.startsWith('/docs/') ||
|
|
208
|
-
url.pathname.startsWith('/code/')
|
|
209
|
-
|
|
355
|
+
url.pathname.startsWith('/code/') ||
|
|
356
|
+
url.pathname.startsWith('/external/')) {
|
|
357
|
+
await sendClientShell(res, join(clientDir, 'index.html'), headOnly);
|
|
210
358
|
return;
|
|
211
359
|
}
|
|
212
360
|
send(res, 404, 'text/plain; charset=utf-8', 'Not found', headOnly);
|
|
@@ -217,50 +365,44 @@ export async function startViewServer(ctx, options = {}) {
|
|
|
217
365
|
}
|
|
218
366
|
sendJson(res, 500, { error: error.message }, false);
|
|
219
367
|
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const requestedPort = options.port;
|
|
223
|
-
let port = requestedPort ?? DEFAULT_VIEW_PORT;
|
|
224
|
-
while (true) {
|
|
225
|
-
try {
|
|
226
|
-
await listen(server, host, port);
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
if (requestedPort !== undefined ||
|
|
231
|
-
error.code !== 'EADDRINUSE' ||
|
|
232
|
-
port === 65_535) {
|
|
233
|
-
throw error;
|
|
234
|
-
}
|
|
235
|
-
port++;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
catch (error) {
|
|
240
|
-
unsubscribeStore();
|
|
241
|
-
await store.close();
|
|
242
|
-
throw error;
|
|
243
|
-
}
|
|
244
|
-
const address = server.address();
|
|
245
|
-
if (!address || typeof address === 'string') {
|
|
246
|
-
await new Promise((resolveClose) => server.close(() => resolveClose()));
|
|
247
|
-
unsubscribeStore();
|
|
248
|
-
await store.close();
|
|
249
|
-
throw new Error('Could not determine lat ui server address');
|
|
250
|
-
}
|
|
368
|
+
};
|
|
369
|
+
const app = createLatServerApp({ handle: handleRequest });
|
|
251
370
|
return {
|
|
252
|
-
|
|
371
|
+
app,
|
|
253
372
|
store,
|
|
254
|
-
url: `http://${host}:${address.port}/`,
|
|
255
373
|
close: async () => {
|
|
374
|
+
clearInterval(heartbeat);
|
|
256
375
|
unsubscribeStore();
|
|
257
376
|
for (const client of eventClients)
|
|
258
377
|
client.end();
|
|
259
378
|
eventClients.clear();
|
|
260
379
|
await store.close();
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
/** Start the loopback server used by `lat ui`. */
|
|
384
|
+
export async function startViewServer(ctx, options = {}) {
|
|
385
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
386
|
+
const view = await createViewApp(ctx, options);
|
|
387
|
+
let running;
|
|
388
|
+
try {
|
|
389
|
+
const requestedPort = options.port;
|
|
390
|
+
running = await listenLatServer(view.app, {
|
|
391
|
+
host,
|
|
392
|
+
port: requestedPort ?? DEFAULT_VIEW_PORT,
|
|
393
|
+
findAvailablePort: requestedPort === undefined,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
await view.close();
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
server: running.server,
|
|
402
|
+
store: view.store,
|
|
403
|
+
url: running.url,
|
|
404
|
+
close: async () => {
|
|
405
|
+
await Promise.all([running.close(), view.close()]);
|
|
264
406
|
},
|
|
265
407
|
};
|
|
266
408
|
}
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import { extname
|
|
2
|
-
import {
|
|
1
|
+
import { extname } from 'node:path';
|
|
2
|
+
import { normalizeRepositoryPath } from '../repository-path.js';
|
|
3
|
+
import { isSourceFileExtension } from '../source-formats.js';
|
|
3
4
|
/** Normalize a supported source wiki-link target for view indexes and routes. */
|
|
4
5
|
export function viewSourceTarget(target) {
|
|
5
6
|
const hash = target.indexOf('#');
|
|
6
7
|
const authoredPath = hash === -1 ? target : target.slice(0, hash);
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
posix.isAbsolute(authoredPath) ||
|
|
10
|
-
!SOURCE_EXTENSIONS.has(extname(authoredPath))) {
|
|
8
|
+
const path = normalizeRepositoryPath(authoredPath);
|
|
9
|
+
if (!path || !isSourceFileExtension(extname(path)))
|
|
11
10
|
return null;
|
|
12
|
-
}
|
|
13
|
-
const path = posix.normalize(authoredPath).replace(/^\.\//, '');
|
|
14
11
|
const symbol = hash === -1 ? '' : target.slice(hash + 1);
|
|
15
12
|
const fileKey = path.toLowerCase();
|
|
16
13
|
return {
|