@shunyasasaki/mkserve 0.1.1
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 +115 -0
- package/dist/cli.mjs +613 -0
- package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/client/assets/abnfDiagram-O67JEVCF-BniYQwXa.js +1 -0
- package/dist/client/assets/arc-BJA87daU.js +1 -0
- package/dist/client/assets/architecture-7GRP2DOG-Brw-cPmm.js +1 -0
- package/dist/client/assets/architectureDiagram-NJMV4G6O-BNbYrQOP.js +36 -0
- package/dist/client/assets/array-BifhSqXX.js +1 -0
- package/dist/client/assets/blockDiagram-BEXU5L5S-DFIHGD-G.js +140 -0
- package/dist/client/assets/c4Diagram-YGBWAQC7-BkFaWKQU.js +38 -0
- package/dist/client/assets/channel-BrqIJdOD.js +1 -0
- package/dist/client/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/dist/client/assets/chunk-3FUC2YCW-B4uxGZek.js +2 -0
- package/dist/client/assets/chunk-5DYCD2WN-CnbWAgyo.js +1 -0
- package/dist/client/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/dist/client/assets/chunk-742MDFTN-CSlr64s4.js +1 -0
- package/dist/client/assets/chunk-7INBJB4K-CelZsjbp.js +62 -0
- package/dist/client/assets/chunk-7M6MHVWA-De6KLJmg.js +213 -0
- package/dist/client/assets/chunk-7PRAP22T-CE6VUtLz.js +1 -0
- package/dist/client/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/dist/client/assets/chunk-FOHPRMQF-DzxwRta7.js +161 -0
- package/dist/client/assets/chunk-GTNCS2PH-DfgoKLZ5.js +88 -0
- package/dist/client/assets/chunk-GWA4HPMP-CPJ61ePi.js +1 -0
- package/dist/client/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/dist/client/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/dist/client/assets/chunk-MBY4JIJT--fcyBAwh.js +72 -0
- package/dist/client/assets/chunk-NETBCI7D-Rdu9faBx.js +1 -0
- package/dist/client/assets/chunk-O7XYJQB3-Bqi4DWie.js +128 -0
- package/dist/client/assets/chunk-UA2S7LBM-BjrJ4RAw.js +1 -0
- package/dist/client/assets/chunk-WEXAMYUT-CnyLp8pC.js +1 -0
- package/dist/client/assets/chunk-XXDRQBXY-C1_eEj17.js +1 -0
- package/dist/client/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/dist/client/assets/chunk-Z7XXMR3K-BFUBfhD1.js +10 -0
- package/dist/client/assets/chunk-ZIGJFQKS-8PrSySWh.js +2 -0
- package/dist/client/assets/classDiagram-v2-NBCMYWYE-KufjDRbB.js +217 -0
- package/dist/client/assets/cose-bilkent-JH36ORCC-Bm2Bhb7n.js +1 -0
- package/dist/client/assets/cynefin-OW5HDTMX-H13mnOIn.js +1 -0
- package/dist/client/assets/cynefinDiagram-VND7K2PF-CI5Kf3Kt.js +62 -0
- package/dist/client/assets/cytoscape.esm-Yq6u8L66.js +321 -0
- package/dist/client/assets/dagre-6A5THRUB-DtRiYFPQ.js +4 -0
- package/dist/client/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/dist/client/assets/diagram-22UHCM2B-DqFq9NIL.js +200 -0
- package/dist/client/assets/diagram-3UASUU5V-C2qQgCvw.js +24 -0
- package/dist/client/assets/diagram-ATOU4E4O-B6wVKZ5z.js +3 -0
- package/dist/client/assets/diagram-CDSNMT55-owh05Yfi.js +30 -0
- package/dist/client/assets/diagram-MLGK6HIB-C55nJ_CR.js +24 -0
- package/dist/client/assets/diagram-MPIPVDR6-DeYo8-yc.js +41 -0
- package/dist/client/assets/dist-Bfr8PhAA.js +1 -0
- package/dist/client/assets/ebnfDiagram-ZINNZB2B-BoJE7Elt.js +1 -0
- package/dist/client/assets/elk-276RUBZZ-F1vDPlT8.js +27 -0
- package/dist/client/assets/erDiagram-OPXOYQCR-JFHuQAWq.js +99 -0
- package/dist/client/assets/eventmodeling-NTZA5JFV-BDnaXJON.js +1 -0
- package/dist/client/assets/flowDiagram-KWPJA3E3-DC2FrXkH.js +1 -0
- package/dist/client/assets/ganttDiagram-FUAMR5RP-C5psbznc.js +292 -0
- package/dist/client/assets/gitGraph-4MIJSDKK-CUDLWiCq.js +1 -0
- package/dist/client/assets/gitGraphDiagram-X574FWY7-Cy-gNPac.js +106 -0
- package/dist/client/assets/graphlib-DS17s2tU.js +1 -0
- package/dist/client/assets/index-BEY9E2Q9.js +2 -0
- package/dist/client/assets/index-FULGulkF.css +2 -0
- package/dist/client/assets/info-A6RAGUB7-mIpFX_UH.js +1 -0
- package/dist/client/assets/infoDiagram-VRGFBTTK-DnkrYsVs.js +2 -0
- package/dist/client/assets/init-C-OQMol4.js +1 -0
- package/dist/client/assets/ishikawaDiagram-OU5B5YK6-_u6_xJba.js +70 -0
- package/dist/client/assets/journeyDiagram-ZHPQQLJL-CY5HU_eC.js +139 -0
- package/dist/client/assets/kanban-definition-PNTS6WVX-DvzRwcD7.js +89 -0
- package/dist/client/assets/katex-ZlcWpGUi.js +257 -0
- package/dist/client/assets/line-B4MFnJjP.js +1 -0
- package/dist/client/assets/linear-ZUqVn2xB.js +1 -0
- package/dist/client/assets/mermaid-parser.core-DYtj8_hH.js +7 -0
- package/dist/client/assets/mermaid.core-DQoUVKoE.js +44 -0
- package/dist/client/assets/mindmap-definition-NLK3R4M7-yqrZgVjd.js +96 -0
- package/dist/client/assets/ordinal-BDEzSJ7C.js +1 -0
- package/dist/client/assets/packet-AYTQ26CC-B26SBXWV.js +1 -0
- package/dist/client/assets/path-fybaL0A-.js +1 -0
- package/dist/client/assets/pegDiagram-GJSIUBJH-BfvMLRyD.js +1 -0
- package/dist/client/assets/pie-WAS4IAKB-DBtOIS90.js +1 -0
- package/dist/client/assets/pieDiagram-5QR66LMP-MLPjQpFM.js +39 -0
- package/dist/client/assets/quadrantDiagram-O4NWA36T-CprY7yVN.js +7 -0
- package/dist/client/assets/radar-RG4KPBEZ-Ced1CHqa.js +1 -0
- package/dist/client/assets/railroad-74A4TZTK-BsND1cV-.js +1 -0
- package/dist/client/assets/railroad-abnf-HS5TGJTU-DiXPG6oV.js +1 -0
- package/dist/client/assets/railroad-ebnf-LZEXJU2U-DgNTMybz.js +1 -0
- package/dist/client/assets/railroad-peg-WCYAUIDC-CvY-xH_Q.js +1 -0
- package/dist/client/assets/railroadDiagram-XR7U4H2S-BeHaiLp2.js +1 -0
- package/dist/client/assets/requirementDiagram-PLB6GJNP-Ck-7Hpab.js +84 -0
- package/dist/client/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/dist/client/assets/sankeyDiagram-IPEJSGJF-DmstfGGj.js +40 -0
- package/dist/client/assets/sequenceDiagram-PO4LG4MO-wco_mvTt.js +169 -0
- package/dist/client/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/dist/client/assets/src-D_iIdUkB.js +1 -0
- package/dist/client/assets/stateDiagram-v2-GCMORJYK-B1l0Xks5.js +291 -0
- package/dist/client/assets/swimlanes-2SLR337P-0YucaaVy.js +1 -0
- package/dist/client/assets/swimlanesDiagram-TC7HE7FX-Dx92YAeU.js +8 -0
- package/dist/client/assets/timeline-definition-EJHVYXUP-BvXqIXyV.js +120 -0
- package/dist/client/assets/treeView-Q6P3EWNA-DrBl-X_2.js +1 -0
- package/dist/client/assets/treemap-WGGIJYW6-bl_fzdzs.js +1 -0
- package/dist/client/assets/usecaseDiagram-POWQR4AR-D_lJYuuq.js +328 -0
- package/dist/client/assets/vennDiagram-UO4OBE2U-BCmaseck.js +34 -0
- package/dist/client/assets/wardley-WFR3VGLG-B2g3_G7F.js +1 -0
- package/dist/client/assets/wardleyDiagram-VNRHLVJA-CZ-FrOG9.js +78 -0
- package/dist/client/assets/xychartDiagram-PMCCYNJV-C3xVeS8n.js +7 -0
- package/dist/client/index.html +59 -0
- package/package.json +96 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { basename, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { defineCommand, runMain } from "citty";
|
|
4
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
5
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import fastifyStatic from "@fastify/static";
|
|
8
|
+
import { consola } from "consola";
|
|
9
|
+
import Fastify from "fastify";
|
|
10
|
+
import getPort from "get-port";
|
|
11
|
+
import open from "open";
|
|
12
|
+
import rehypeShikiFromHighlighter from "@shikijs/rehype/core";
|
|
13
|
+
import { toString } from "hast-util-to-string";
|
|
14
|
+
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
15
|
+
import rehypeKatex from "rehype-katex";
|
|
16
|
+
import rehypeRaw from "rehype-raw";
|
|
17
|
+
import rehypeSlug from "rehype-slug";
|
|
18
|
+
import rehypeStringify from "rehype-stringify";
|
|
19
|
+
import remarkFrontmatter from "remark-frontmatter";
|
|
20
|
+
import remarkGfm from "remark-gfm";
|
|
21
|
+
import { remarkAlert } from "remark-github-blockquote-alert";
|
|
22
|
+
import remarkMath from "remark-math";
|
|
23
|
+
import remarkParse from "remark-parse";
|
|
24
|
+
import remarkRehype from "remark-rehype";
|
|
25
|
+
import { createHighlighter } from "shiki";
|
|
26
|
+
import { unified } from "unified";
|
|
27
|
+
import { visit } from "unist-util-visit";
|
|
28
|
+
import { watch } from "chokidar";
|
|
29
|
+
import ignore from "ignore";
|
|
30
|
+
//#region package.json
|
|
31
|
+
var version = "0.1.1";
|
|
32
|
+
var description = "Serve a workspace of Markdown files with a live-reloading preview.";
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/shared/paths.ts
|
|
35
|
+
/** URL-encodes each segment of a POSIX path, keeping the slashes. */
|
|
36
|
+
function encodePath(path) {
|
|
37
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
38
|
+
}
|
|
39
|
+
/** Inverse of {@link encodePath}; returns the input if it is malformed. */
|
|
40
|
+
function decodePath(path) {
|
|
41
|
+
try {
|
|
42
|
+
return decodeURIComponent(path);
|
|
43
|
+
} catch {
|
|
44
|
+
return path;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/shared/types.ts
|
|
49
|
+
/** Extensions treated as Markdown documents. */
|
|
50
|
+
const MARKDOWN_EXTENSIONS = [".md", ".markdown"];
|
|
51
|
+
/** Returns whether `path` names a Markdown document. */
|
|
52
|
+
function isMarkdownPath(path) {
|
|
53
|
+
const lower = path.toLowerCase();
|
|
54
|
+
return MARKDOWN_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/server/render.ts
|
|
58
|
+
/** Languages loaded up front; others are loaded on first use. */
|
|
59
|
+
const PRELOADED_LANGUAGES = [
|
|
60
|
+
"bash",
|
|
61
|
+
"css",
|
|
62
|
+
"diff",
|
|
63
|
+
"html",
|
|
64
|
+
"javascript",
|
|
65
|
+
"json",
|
|
66
|
+
"jsx",
|
|
67
|
+
"markdown",
|
|
68
|
+
"python",
|
|
69
|
+
"shell",
|
|
70
|
+
"sql",
|
|
71
|
+
"toml",
|
|
72
|
+
"tsx",
|
|
73
|
+
"typescript",
|
|
74
|
+
"yaml"
|
|
75
|
+
];
|
|
76
|
+
const THEMES = {
|
|
77
|
+
light: "github-light",
|
|
78
|
+
dark: "github-dark"
|
|
79
|
+
};
|
|
80
|
+
const AUTOLINK_OPTIONS = {
|
|
81
|
+
behavior: "append",
|
|
82
|
+
properties: {
|
|
83
|
+
className: ["heading-anchor"],
|
|
84
|
+
ariaHidden: "true"
|
|
85
|
+
},
|
|
86
|
+
content: {
|
|
87
|
+
type: "text",
|
|
88
|
+
value: "#"
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/** Elements whose URL attributes point at workspace files. */
|
|
92
|
+
const URL_ATTRIBUTES = {
|
|
93
|
+
a: ["href"],
|
|
94
|
+
img: ["src"],
|
|
95
|
+
source: ["src"],
|
|
96
|
+
video: ["src", "poster"],
|
|
97
|
+
audio: ["src"]
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Creates the unified pipeline (CommonMark + GFM, math, alerts, Mermaid,
|
|
101
|
+
* Shiki). The Shiki highlighter is created once and shared by all renders.
|
|
102
|
+
*/
|
|
103
|
+
async function createRenderer() {
|
|
104
|
+
const highlighter = await createHighlighter({
|
|
105
|
+
themes: Object.values(THEMES),
|
|
106
|
+
langs: PRELOADED_LANGUAGES
|
|
107
|
+
});
|
|
108
|
+
const processor = unified().use(remarkParse).use(remarkFrontmatter).use(remarkGfm).use(remarkMath).use(remarkAlert).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw).use(rehypeSlug).use(rehypeAutolinkHeadings, AUTOLINK_OPTIONS).use(rehypeKatex, { strict: "ignore" }).use(rehypeMermaid).use(rehypeShikiFromHighlighter, highlighter, {
|
|
109
|
+
themes: THEMES,
|
|
110
|
+
lazy: true,
|
|
111
|
+
fallbackLanguage: "text"
|
|
112
|
+
}).use(rehypeRewriteUrls).use(rehypeExtractTitle).use(rehypeStringify);
|
|
113
|
+
return { async render(markdown, docPath) {
|
|
114
|
+
const file = await processor.process({
|
|
115
|
+
value: markdown,
|
|
116
|
+
data: { docPath }
|
|
117
|
+
});
|
|
118
|
+
return {
|
|
119
|
+
html: String(file),
|
|
120
|
+
title: file.data.title
|
|
121
|
+
};
|
|
122
|
+
} };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Turns ```` ```mermaid ```` code blocks into `<pre class="mermaid">` so the
|
|
126
|
+
* client can render them (and Shiki leaves them alone).
|
|
127
|
+
*/
|
|
128
|
+
function rehypeMermaid() {
|
|
129
|
+
return (tree) => {
|
|
130
|
+
visit(tree, "element", (node) => {
|
|
131
|
+
const code = node.children[0];
|
|
132
|
+
if (node.tagName === "pre" && code?.type === "element" && code.tagName === "code" && classNames(code).includes("language-mermaid")) {
|
|
133
|
+
node.properties = { className: ["mermaid"] };
|
|
134
|
+
node.children = [{
|
|
135
|
+
type: "text",
|
|
136
|
+
value: toString(code)
|
|
137
|
+
}];
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Rewrites relative links: Markdown links become SPA routes, other files are
|
|
144
|
+
* served through `/@ws/`, and external links open in a new tab.
|
|
145
|
+
*/
|
|
146
|
+
function rehypeRewriteUrls() {
|
|
147
|
+
return (tree, file) => {
|
|
148
|
+
const docDir = posix.dirname(file.data.docPath ?? "");
|
|
149
|
+
visit(tree, "element", (node) => {
|
|
150
|
+
for (const attr of URL_ATTRIBUTES[node.tagName] ?? []) {
|
|
151
|
+
const value = node.properties[attr];
|
|
152
|
+
if (typeof value !== "string") continue;
|
|
153
|
+
if (isExternal(value)) {
|
|
154
|
+
if (node.tagName === "a") {
|
|
155
|
+
node.properties.target = "_blank";
|
|
156
|
+
node.properties.rel = ["noopener", "noreferrer"];
|
|
157
|
+
}
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
node.properties[attr] = rewriteUrl(value, docDir, node.tagName === "a");
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function rehypeExtractTitle() {
|
|
166
|
+
return (tree, file) => {
|
|
167
|
+
visit(tree, "element", (node) => {
|
|
168
|
+
if (node.tagName === "h1" && file.data.title === void 0) file.data.title = toString(node).replace(/#$/, "").trim();
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function isExternal(url) {
|
|
173
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith("//");
|
|
174
|
+
}
|
|
175
|
+
/** Exported for tests. */
|
|
176
|
+
function rewriteUrl(url, docDir, isLink) {
|
|
177
|
+
if (url === "" || url.startsWith("#")) return url;
|
|
178
|
+
const cut = url.search(/[?#]/);
|
|
179
|
+
const pathPart = cut === -1 ? url : url.slice(0, cut);
|
|
180
|
+
const suffix = cut === -1 ? "" : url.slice(cut);
|
|
181
|
+
let decoded;
|
|
182
|
+
try {
|
|
183
|
+
decoded = decodeURI(pathPart);
|
|
184
|
+
} catch {
|
|
185
|
+
return url;
|
|
186
|
+
}
|
|
187
|
+
const resolved = decoded.startsWith("/") ? posix.normalize(decoded.slice(1)) : posix.join(docDir, decoded);
|
|
188
|
+
if (resolved.startsWith("..")) return url;
|
|
189
|
+
return (isLink && isMarkdownPath(resolved) ? "/" : "/@ws/") + encodePath(resolved) + suffix;
|
|
190
|
+
}
|
|
191
|
+
function classNames(node) {
|
|
192
|
+
const value = node.properties.className;
|
|
193
|
+
return Array.isArray(value) ? value.map(String) : [];
|
|
194
|
+
}
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/server/safe_path.ts
|
|
197
|
+
/**
|
|
198
|
+
* Resolves a workspace-relative path to an absolute one, or returns
|
|
199
|
+
* `undefined` when it would escape `root` (including through symlinks).
|
|
200
|
+
*/
|
|
201
|
+
function resolveInRoot(root, relPath) {
|
|
202
|
+
if (relPath.includes("\0")) return;
|
|
203
|
+
const abs = resolve(root, relPath.replace(/^\/+/, ""));
|
|
204
|
+
if (!isInside(root, abs)) return;
|
|
205
|
+
try {
|
|
206
|
+
if (!isInside(realpathSync(root), realpathSync(abs))) return;
|
|
207
|
+
} catch {}
|
|
208
|
+
return abs;
|
|
209
|
+
}
|
|
210
|
+
/** Converts an absolute path inside `root` to a POSIX relative path. */
|
|
211
|
+
function toRelPosix(root, abs) {
|
|
212
|
+
return relative(root, abs).split(sep).join("/");
|
|
213
|
+
}
|
|
214
|
+
function isInside(root, abs) {
|
|
215
|
+
const rel = relative(root, abs);
|
|
216
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
217
|
+
}
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/server/sse.ts
|
|
220
|
+
const PING_INTERVAL_MS = 3e4;
|
|
221
|
+
/** Registry of connected `EventSource` clients. */
|
|
222
|
+
var EventHub = class {
|
|
223
|
+
clients = /* @__PURE__ */ new Set();
|
|
224
|
+
pingTimer;
|
|
225
|
+
constructor() {
|
|
226
|
+
this.pingTimer = setInterval(() => this.write("ping", {}), PING_INTERVAL_MS);
|
|
227
|
+
this.pingTimer.unref();
|
|
228
|
+
}
|
|
229
|
+
/** Number of connected clients. */
|
|
230
|
+
get size() {
|
|
231
|
+
return this.clients.size;
|
|
232
|
+
}
|
|
233
|
+
/** Starts an SSE stream on `res` and keeps it until the client leaves. */
|
|
234
|
+
add(res) {
|
|
235
|
+
res.writeHead(200, {
|
|
236
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
237
|
+
"cache-control": "no-cache",
|
|
238
|
+
connection: "keep-alive",
|
|
239
|
+
"x-accel-buffering": "no"
|
|
240
|
+
});
|
|
241
|
+
res.write("retry: 1000\n\n");
|
|
242
|
+
this.clients.add(res);
|
|
243
|
+
res.on("close", () => this.clients.delete(res));
|
|
244
|
+
}
|
|
245
|
+
/** Sends an event to every client. */
|
|
246
|
+
broadcast(event, data) {
|
|
247
|
+
this.write(event, data);
|
|
248
|
+
}
|
|
249
|
+
/** Ends all streams so the HTTP server can shut down. */
|
|
250
|
+
close() {
|
|
251
|
+
clearInterval(this.pingTimer);
|
|
252
|
+
for (const res of this.clients) res.end();
|
|
253
|
+
this.clients.clear();
|
|
254
|
+
}
|
|
255
|
+
write(event, data) {
|
|
256
|
+
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
257
|
+
for (const res of this.clients) res.write(message);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
//#endregion
|
|
261
|
+
//#region src/server/tree.ts
|
|
262
|
+
/**
|
|
263
|
+
* Builds the Markdown-only tree of `root`. Directories without Markdown
|
|
264
|
+
* files (recursively) are pruned; ignored and hidden entries are skipped.
|
|
265
|
+
*/
|
|
266
|
+
async function buildTree(root, ignore) {
|
|
267
|
+
const children = await readDir(root, "", ignore);
|
|
268
|
+
return {
|
|
269
|
+
name: basename(root),
|
|
270
|
+
path: "",
|
|
271
|
+
type: "dir",
|
|
272
|
+
children
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
async function readDir(root, relDir, ignore) {
|
|
276
|
+
let entries;
|
|
277
|
+
try {
|
|
278
|
+
entries = await readdir(join(root, relDir), { withFileTypes: true });
|
|
279
|
+
} catch {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
const nodes = [];
|
|
283
|
+
for (const entry of entries) {
|
|
284
|
+
const path = relDir === "" ? entry.name : `${relDir}/${entry.name}`;
|
|
285
|
+
let isDir = entry.isDirectory();
|
|
286
|
+
let isFile = entry.isFile();
|
|
287
|
+
if (entry.isSymbolicLink()) {
|
|
288
|
+
const target = await stat(join(root, path)).catch(() => void 0);
|
|
289
|
+
isDir = false;
|
|
290
|
+
isFile = target?.isFile() ?? false;
|
|
291
|
+
}
|
|
292
|
+
if (!isDir && !isFile || ignore.isIgnored(path, isDir)) continue;
|
|
293
|
+
if (isDir) {
|
|
294
|
+
const children = await readDir(root, path, ignore);
|
|
295
|
+
if (children.length > 0) nodes.push({
|
|
296
|
+
name: entry.name,
|
|
297
|
+
path,
|
|
298
|
+
type: "dir",
|
|
299
|
+
children
|
|
300
|
+
});
|
|
301
|
+
} else if (isMarkdownPath(entry.name)) nodes.push({
|
|
302
|
+
name: entry.name,
|
|
303
|
+
path,
|
|
304
|
+
type: "file"
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
return nodes.sort(compareNodes);
|
|
308
|
+
}
|
|
309
|
+
function compareNodes(a, b) {
|
|
310
|
+
if (a.type !== b.type) return a.type === "dir" ? -1 : 1;
|
|
311
|
+
return a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
|
|
312
|
+
}
|
|
313
|
+
//#endregion
|
|
314
|
+
//#region src/server/watcher.ts
|
|
315
|
+
const DEBOUNCE_MS = 50;
|
|
316
|
+
/**
|
|
317
|
+
* Watches the workspace and broadcasts `change` (Markdown edited, added, or
|
|
318
|
+
* removed), `tree` (structure changed), and `asset` (other file edited).
|
|
319
|
+
*/
|
|
320
|
+
function startWatcher(options) {
|
|
321
|
+
const { root, ignore, hub } = options;
|
|
322
|
+
const timers = /* @__PURE__ */ new Map();
|
|
323
|
+
function debounced(key, fn) {
|
|
324
|
+
clearTimeout(timers.get(key));
|
|
325
|
+
timers.set(key, setTimeout(() => {
|
|
326
|
+
timers.delete(key);
|
|
327
|
+
fn();
|
|
328
|
+
}, DEBOUNCE_MS));
|
|
329
|
+
}
|
|
330
|
+
function treeChanged() {
|
|
331
|
+
debounced("\0tree", () => {
|
|
332
|
+
options.onTreeChange();
|
|
333
|
+
hub.broadcast("tree", {});
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const watcher = watch(root, {
|
|
337
|
+
ignoreInitial: true,
|
|
338
|
+
awaitWriteFinish: {
|
|
339
|
+
stabilityThreshold: 100,
|
|
340
|
+
pollInterval: 20
|
|
341
|
+
},
|
|
342
|
+
ignored: (absPath, stats) => {
|
|
343
|
+
const rel = toRelPosix(root, absPath);
|
|
344
|
+
if (rel === "") return false;
|
|
345
|
+
if (basename(rel) === ".gitignore") {
|
|
346
|
+
const dir = posix.dirname(rel);
|
|
347
|
+
return dir !== "." && ignore.isIgnored(dir, true);
|
|
348
|
+
}
|
|
349
|
+
return ignore.isIgnored(rel, stats?.isDirectory() ?? false);
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
watcher.on("all", (event, absPath) => {
|
|
353
|
+
const path = toRelPosix(root, absPath);
|
|
354
|
+
if (basename(path) === ".gitignore") {
|
|
355
|
+
ignore.invalidate();
|
|
356
|
+
treeChanged();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (event === "addDir" || event === "unlinkDir") {
|
|
360
|
+
treeChanged();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (!isMarkdownPath(path)) {
|
|
364
|
+
debounced(`asset:${path}`, () => hub.broadcast("asset", { path }));
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (event === "add" || event === "unlink") treeChanged();
|
|
368
|
+
debounced(`change:${path}`, () => hub.broadcast("change", { path }));
|
|
369
|
+
});
|
|
370
|
+
return watcher;
|
|
371
|
+
}
|
|
372
|
+
//#endregion
|
|
373
|
+
//#region src/server/workspace_ignore.ts
|
|
374
|
+
/** Directory names that are never part of the workspace view. */
|
|
375
|
+
const ALWAYS_IGNORED = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
376
|
+
/**
|
|
377
|
+
* Decides whether workspace paths are hidden, honoring `.gitignore` files at
|
|
378
|
+
* every directory level. Rules are loaded lazily and cached until
|
|
379
|
+
* {@link WorkspaceIgnore.invalidate} is called.
|
|
380
|
+
*/
|
|
381
|
+
var WorkspaceIgnore = class {
|
|
382
|
+
root;
|
|
383
|
+
rules = /* @__PURE__ */ new Map();
|
|
384
|
+
constructor(root) {
|
|
385
|
+
this.root = root;
|
|
386
|
+
}
|
|
387
|
+
/** Drops cached rules, e.g. after a `.gitignore` changed. */
|
|
388
|
+
invalidate() {
|
|
389
|
+
this.rules.clear();
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Returns whether the POSIX relative path is hidden: a dot-entry, an
|
|
393
|
+
* always-ignored directory, or matched by a `.gitignore` (or inside a
|
|
394
|
+
* directory that is).
|
|
395
|
+
*/
|
|
396
|
+
isIgnored(relPath, isDir) {
|
|
397
|
+
const segments = relPath.split("/").filter((s) => s !== "" && s !== ".");
|
|
398
|
+
for (let i = 0; i < segments.length; i++) {
|
|
399
|
+
const name = segments[i];
|
|
400
|
+
if (name.startsWith(".") || ALWAYS_IGNORED.has(name)) return true;
|
|
401
|
+
const entryIsDir = i < segments.length - 1 || isDir;
|
|
402
|
+
if (this.matchesGitignore(segments, i, entryIsDir)) return true;
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
/** Checks segments[0..=index] against every ancestor `.gitignore`. */
|
|
407
|
+
matchesGitignore(segments, index, isDir) {
|
|
408
|
+
for (let base = 0; base <= index; base++) {
|
|
409
|
+
const rules = this.load(segments.slice(0, base).join("/"));
|
|
410
|
+
if (!rules) continue;
|
|
411
|
+
const sub = segments.slice(base, index + 1).join("/");
|
|
412
|
+
if (rules.ignores(isDir ? `${sub}/` : sub)) return true;
|
|
413
|
+
}
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
load(dir) {
|
|
417
|
+
const cached = this.rules.get(dir);
|
|
418
|
+
if (cached !== void 0) return cached;
|
|
419
|
+
let rules = null;
|
|
420
|
+
try {
|
|
421
|
+
const text = readFileSync(join(this.root, dir, ".gitignore"), "utf8");
|
|
422
|
+
rules = ignore().add(text);
|
|
423
|
+
} catch {}
|
|
424
|
+
this.rules.set(dir, rules);
|
|
425
|
+
return rules;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
//#endregion
|
|
429
|
+
//#region src/server/app.ts
|
|
430
|
+
/** Built client, located next to the bundled `dist/cli.mjs`. */
|
|
431
|
+
const CLIENT_DIR = fileURLToPath(new URL("./client/", import.meta.url));
|
|
432
|
+
const CLIENT_NOT_BUILT_HTML = "<!doctype html><title>mkserve</title><p>The client is not built. Run <code>pnpm build</code>.</p>";
|
|
433
|
+
/** Creates the Fastify app (routes, static files, watcher) without listening. */
|
|
434
|
+
async function buildApp(options) {
|
|
435
|
+
const { root } = options;
|
|
436
|
+
const clientDir = options.clientDir ?? CLIENT_DIR;
|
|
437
|
+
const renderer = options.renderer ?? await createRenderer();
|
|
438
|
+
const ignore = new WorkspaceIgnore(root);
|
|
439
|
+
const hub = new EventHub();
|
|
440
|
+
const app = Fastify({ forceCloseConnections: true });
|
|
441
|
+
let treeCache;
|
|
442
|
+
function getTree() {
|
|
443
|
+
if (!options.watch) return buildTree(root, ignore);
|
|
444
|
+
treeCache ??= buildTree(root, ignore);
|
|
445
|
+
return treeCache;
|
|
446
|
+
}
|
|
447
|
+
if (options.watch) {
|
|
448
|
+
const watcher = startWatcher({
|
|
449
|
+
root,
|
|
450
|
+
ignore,
|
|
451
|
+
hub,
|
|
452
|
+
onTreeChange: () => {
|
|
453
|
+
treeCache = void 0;
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
app.addHook("onClose", async () => {
|
|
457
|
+
await watcher.close();
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
app.addHook("preClose", async () => hub.close());
|
|
461
|
+
await app.register(fastifyStatic, {
|
|
462
|
+
root,
|
|
463
|
+
serve: false
|
|
464
|
+
});
|
|
465
|
+
const indexHtml = existsSync(join(clientDir, "index.html")) ? readFileSync(join(clientDir, "index.html"), "utf8") : CLIENT_NOT_BUILT_HTML;
|
|
466
|
+
if (existsSync(clientDir)) await app.register(fastifyStatic, {
|
|
467
|
+
root: clientDir,
|
|
468
|
+
prefix: "/",
|
|
469
|
+
index: false,
|
|
470
|
+
wildcard: false,
|
|
471
|
+
decorateReply: false
|
|
472
|
+
});
|
|
473
|
+
app.get("/api/info", async () => {
|
|
474
|
+
return {
|
|
475
|
+
name: basename(root),
|
|
476
|
+
version,
|
|
477
|
+
watch: options.watch
|
|
478
|
+
};
|
|
479
|
+
});
|
|
480
|
+
app.get("/api/tree", () => getTree());
|
|
481
|
+
app.get("/api/render", async (request, reply) => {
|
|
482
|
+
const relPath = request.query.path;
|
|
483
|
+
if (!relPath || !isMarkdownPath(relPath)) return sendError(reply, 400, "A Markdown path is required");
|
|
484
|
+
const abs = resolveInRoot(root, relPath);
|
|
485
|
+
if (abs === void 0) return sendError(reply, 400, "Path is outside the workspace");
|
|
486
|
+
const path = toRelPosix(root, abs);
|
|
487
|
+
if (ignore.isIgnored(path, false)) return sendError(reply, 404, "Not found");
|
|
488
|
+
let source;
|
|
489
|
+
let mtime;
|
|
490
|
+
try {
|
|
491
|
+
[source, mtime] = await Promise.all([readFile(abs, "utf8"), stat(abs).then((s) => s.mtimeMs)]);
|
|
492
|
+
} catch {
|
|
493
|
+
return sendError(reply, 404, "Not found");
|
|
494
|
+
}
|
|
495
|
+
const { html, title } = await renderer.render(source, path);
|
|
496
|
+
return {
|
|
497
|
+
path,
|
|
498
|
+
html,
|
|
499
|
+
title: title ?? basename(path),
|
|
500
|
+
mtime
|
|
501
|
+
};
|
|
502
|
+
});
|
|
503
|
+
app.get("/api/events", (request, reply) => {
|
|
504
|
+
if (!options.watch) return sendError(reply, 404, "Live reload is disabled");
|
|
505
|
+
reply.hijack();
|
|
506
|
+
hub.add(reply.raw);
|
|
507
|
+
return reply;
|
|
508
|
+
});
|
|
509
|
+
app.get("/@ws/*", (request, reply) => {
|
|
510
|
+
const abs = resolveInRoot(root, decodePath(request.params["*"]));
|
|
511
|
+
if (abs === void 0) return sendError(reply, 400, "Path is outside the workspace");
|
|
512
|
+
const path = toRelPosix(root, abs);
|
|
513
|
+
if (path === "" || ignore.isIgnored(path, false)) return sendError(reply, 404, "Not found");
|
|
514
|
+
return reply.header("cache-control", "no-cache").sendFile(path);
|
|
515
|
+
});
|
|
516
|
+
app.setNotFoundHandler((request, reply) => {
|
|
517
|
+
const wantsHtml = request.headers.accept?.includes("text/html") ?? false;
|
|
518
|
+
const isApi = /^\/(api|@ws)\//.test(request.url);
|
|
519
|
+
if (request.method === "GET" && wantsHtml && !isApi) return reply.type("text/html; charset=utf-8").send(indexHtml);
|
|
520
|
+
return sendError(reply, 404, "Not found");
|
|
521
|
+
});
|
|
522
|
+
return app;
|
|
523
|
+
}
|
|
524
|
+
function sendError(reply, statusCode, message) {
|
|
525
|
+
return reply.code(statusCode).send({ error: message });
|
|
526
|
+
}
|
|
527
|
+
/** Builds the app, listens, and wires graceful shutdown. */
|
|
528
|
+
async function startServer(options) {
|
|
529
|
+
if (!existsSync(options.root)) {
|
|
530
|
+
consola.error(`Directory not found: ${options.root}`);
|
|
531
|
+
process.exit(1);
|
|
532
|
+
}
|
|
533
|
+
const app = await buildApp({
|
|
534
|
+
root: options.root,
|
|
535
|
+
watch: options.watch
|
|
536
|
+
});
|
|
537
|
+
const port = options.port ?? await getPort({ port: options.defaultPort });
|
|
538
|
+
try {
|
|
539
|
+
await app.listen({
|
|
540
|
+
port,
|
|
541
|
+
host: options.host
|
|
542
|
+
});
|
|
543
|
+
} catch (err) {
|
|
544
|
+
consola.error(err instanceof Error ? err.message : err);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
const url = `http://${options.host}:${port}`;
|
|
548
|
+
consola.success(`mkserve v${version} → ${url} (root: ${options.root})`);
|
|
549
|
+
if (options.open) await open(url);
|
|
550
|
+
const shutdown = async () => {
|
|
551
|
+
await app.close();
|
|
552
|
+
process.exit(0);
|
|
553
|
+
};
|
|
554
|
+
process.once("SIGINT", shutdown);
|
|
555
|
+
process.once("SIGTERM", shutdown);
|
|
556
|
+
}
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/cli.ts
|
|
559
|
+
const DEFAULT_PORT = 3e3;
|
|
560
|
+
const main = defineCommand({
|
|
561
|
+
meta: {
|
|
562
|
+
name: "mkserve",
|
|
563
|
+
version,
|
|
564
|
+
description
|
|
565
|
+
},
|
|
566
|
+
args: {
|
|
567
|
+
dir: {
|
|
568
|
+
type: "positional",
|
|
569
|
+
description: "Workspace root",
|
|
570
|
+
required: false,
|
|
571
|
+
default: "."
|
|
572
|
+
},
|
|
573
|
+
port: {
|
|
574
|
+
type: "string",
|
|
575
|
+
alias: "p",
|
|
576
|
+
description: `Port to listen on (default: ${DEFAULT_PORT})`
|
|
577
|
+
},
|
|
578
|
+
host: {
|
|
579
|
+
type: "string",
|
|
580
|
+
description: "Bind address",
|
|
581
|
+
default: "127.0.0.1"
|
|
582
|
+
},
|
|
583
|
+
open: {
|
|
584
|
+
type: "boolean",
|
|
585
|
+
alias: "o",
|
|
586
|
+
description: "Open the browser after start",
|
|
587
|
+
default: false
|
|
588
|
+
},
|
|
589
|
+
watch: {
|
|
590
|
+
type: "boolean",
|
|
591
|
+
description: "Reload pages on file changes (use --no-watch to disable)",
|
|
592
|
+
default: true
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
async run({ args }) {
|
|
596
|
+
await startServer({
|
|
597
|
+
root: resolve(args.dir),
|
|
598
|
+
port: args.port === void 0 ? void 0 : parsePort(args.port),
|
|
599
|
+
defaultPort: DEFAULT_PORT,
|
|
600
|
+
host: args.host,
|
|
601
|
+
open: args.open,
|
|
602
|
+
watch: args.watch
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
function parsePort(value) {
|
|
607
|
+
const port = Number(value);
|
|
608
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid port: ${value}`);
|
|
609
|
+
return port;
|
|
610
|
+
}
|
|
611
|
+
runMain(main);
|
|
612
|
+
//#endregion
|
|
613
|
+
export {};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|