marksites 0.1.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.
Files changed (86) hide show
  1. package/README.md +153 -0
  2. package/dist/annotations/model.d.ts +29 -0
  3. package/dist/annotations/model.js +56 -0
  4. package/dist/annotations/storage.d.ts +4 -0
  5. package/dist/annotations/storage.js +31 -0
  6. package/dist/cli/directory.d.ts +4 -0
  7. package/dist/cli/directory.js +3 -0
  8. package/dist/cli/open-browser.d.ts +12 -0
  9. package/dist/cli/open-browser.js +37 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.js +125 -0
  12. package/dist/conversion/directory.d.ts +3 -0
  13. package/dist/conversion/directory.js +208 -0
  14. package/dist/conversion/gitignore.d.ts +8 -0
  15. package/dist/conversion/gitignore.js +43 -0
  16. package/dist/conversion/manifest.d.ts +8 -0
  17. package/dist/conversion/manifest.js +40 -0
  18. package/dist/conversion/navigation.d.ts +4 -0
  19. package/dist/conversion/navigation.js +70 -0
  20. package/dist/conversion/paths.d.ts +10 -0
  21. package/dist/conversion/paths.js +86 -0
  22. package/dist/conversion/rendering.d.ts +5 -0
  23. package/dist/conversion/rendering.js +39 -0
  24. package/dist/conversion/single-file.d.ts +1 -0
  25. package/dist/conversion/single-file.js +35 -0
  26. package/dist/conversion/types.d.ts +39 -0
  27. package/dist/conversion/types.js +1 -0
  28. package/dist/features/annotations/index.d.ts +7 -0
  29. package/dist/features/annotations/index.js +79 -0
  30. package/dist/features/annotations.d.ts +9 -0
  31. package/dist/features/annotations.js +78 -0
  32. package/dist/features/code-blocks/index.d.ts +5 -0
  33. package/dist/features/code-blocks/index.js +97 -0
  34. package/dist/features/code-blocks.d.ts +5 -0
  35. package/dist/features/code-blocks.js +89 -0
  36. package/dist/features/file-tree/index.d.ts +7 -0
  37. package/dist/features/file-tree/index.js +347 -0
  38. package/dist/features/file-tree.d.ts +7 -0
  39. package/dist/features/file-tree.js +325 -0
  40. package/dist/features/header/index.d.ts +4 -0
  41. package/dist/features/header/index.js +47 -0
  42. package/dist/features/header.d.ts +6 -0
  43. package/dist/features/header.js +47 -0
  44. package/dist/features/sidebar/index.d.ts +10 -0
  45. package/dist/features/sidebar/index.js +55 -0
  46. package/dist/features/sidebar.d.ts +13 -0
  47. package/dist/features/sidebar.js +55 -0
  48. package/dist/features/table-of-contents/index.d.ts +16 -0
  49. package/dist/features/table-of-contents/index.js +91 -0
  50. package/dist/features/table-of-contents.d.ts +16 -0
  51. package/dist/features/table-of-contents.js +91 -0
  52. package/dist/features/types.d.ts +6 -0
  53. package/dist/features/types.js +1 -0
  54. package/dist/index.d.ts +2 -0
  55. package/dist/index.js +1 -0
  56. package/dist/markdown-to-html.d.ts +5 -0
  57. package/dist/markdown-to-html.js +77 -0
  58. package/dist/server/annotation-repository.d.ts +35 -0
  59. package/dist/server/annotation-repository.js +184 -0
  60. package/dist/server/api.d.ts +4 -0
  61. package/dist/server/api.js +77 -0
  62. package/dist/server/constants.d.ts +2 -0
  63. package/dist/server/constants.js +2 -0
  64. package/dist/server/html-security.d.ts +4 -0
  65. package/dist/server/html-security.js +61 -0
  66. package/dist/server/response.d.ts +2 -0
  67. package/dist/server/response.js +9 -0
  68. package/dist/server/server.d.ts +3 -0
  69. package/dist/server/server.js +77 -0
  70. package/dist/server/static-files.d.ts +2 -0
  71. package/dist/server/static-files.js +72 -0
  72. package/dist/server/types.d.ts +16 -0
  73. package/dist/server/types.js +1 -0
  74. package/dist/template/document.d.ts +20 -0
  75. package/dist/template/document.js +33 -0
  76. package/dist/template/styles.d.ts +5 -0
  77. package/dist/template/styles.js +109 -0
  78. package/dist/types.d.ts +52 -0
  79. package/dist/types.js +1 -0
  80. package/dist/utils/files.d.ts +1 -0
  81. package/dist/utils/files.js +9 -0
  82. package/dist/utils/html.d.ts +2 -0
  83. package/dist/utils/html.js +17 -0
  84. package/dist/utils/icons.d.ts +8 -0
  85. package/dist/utils/icons.js +24 -0
  86. package/package.json +43 -0
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # marksites
2
+
3
+ Markdownを、GitHub風のスタイルを埋め込んだ単独のHTMLファイルへ変換するTypeScriptライブラリです。
4
+
5
+ ## インストール
6
+
7
+ ```sh
8
+ npm install
9
+ npm run build
10
+ ```
11
+
12
+ ## ライブラリとして使う
13
+
14
+ ```ts
15
+ import { markdownToHtml } from "marksites";
16
+
17
+ const html = markdownToHtml("# Hello", { title: "My page" });
18
+ ```
19
+
20
+ 更新日時を表示する場合は、ISO 8601形式で指定します。CLI変換ではMarkdownファイルの最終更新日時が自動的に使用されます。
21
+
22
+ ```ts
23
+ const html = markdownToHtml(markdown, {
24
+ modifiedAt: "2026-07-17T03:00:00.000Z",
25
+ });
26
+ ```
27
+
28
+ 見出しレベル2から6までの目次と、GitHub互換の見出しアンカーが自動生成されます。ファイルツリーはGitHubのFilesペインと同様、画面左端へ接する全高のサイドバーとして常時表示し、本文を中央、`目次`とコメントを画面右側へ配置します。左サイドバーはヘッダーの四角いペインアイコンで折り畳むことができ、閉じると本文が左へ広がります。閉じた場合はページ上部に同形の復帰アイコンと、従来どおりポップオーバーでファイル一覧を開く`ファイル`ボタンを並べます。サイドバーの閉状態は`file-sidebar=closed`、ポップオーバーは`marksites-files=open`、開いたフォルダは短縮IDを値とする`open` GETパラメータでページ間に引き継がれます。ファイルパス直後のアイコンでパスをコピーでき、右端にはMarkdownの更新日時をブラウザのローカルタイムゾーンで`YYYY-MM-DD HH:mm:ss`形式にして表示します。タイムゾーン名やUTCオフセットは表示しません。JavaScript実行前はUTCの日時を同じ形式で表示します。現在位置に対応する見出しもハイライトされます。狭い画面では右サイドバーを本文上部、ファイルサイドバーを必要なときだけ開く固定オーバーレイとして表示します。
29
+
30
+ 左ファイルサイドバーは幅280pxとし、開閉アイコンは枠線なしで表示します。
31
+
32
+ 画面上端には固定ヘッダーを表示します。右上の月・太陽アイコンでライト/ダークモードを切り替え、地球アイコンで生成UIの日本語/英語を切り替えます。Markdown本文、見出し、ファイル名、コメント本文は翻訳対象にしません。選択状態は`theme`と`lang` GETパラメータでページ間に引き継がれます。
33
+
34
+ ```ts
35
+ const html = markdownToHtml(markdown, {
36
+ tableOfContents: {
37
+ title: "目次",
38
+ minDepth: 2,
39
+ maxDepth: 4,
40
+ },
41
+ });
42
+ ```
43
+
44
+ 目次を無効にする場合は `tableOfContents: false` を指定します。見出しのアンカーIDはそのまま生成されます。
45
+
46
+ 言語名を指定したコードブロックは、`highlight.js` によって自動的にハイライトされます。
47
+ コードブロックには、コードをクリップボードへコピーするボタンと、長い行の折り返しを切り替えるボタンも表示されます。
48
+
49
+ ````markdown
50
+ ```typescript
51
+ const greeting: string = "Hello";
52
+ console.log(greeting);
53
+ ```
54
+ ````
55
+
56
+ ハイライトを無効にすることもできます。
57
+
58
+ ```ts
59
+ const html = markdownToHtml(markdown, { highlight: false });
60
+ ```
61
+
62
+ ## CLIとして使う
63
+
64
+ ```sh
65
+ npx marksites README.md README.html
66
+ ```
67
+
68
+ 入力と出力を両方省略すると、カレントディレクトリ配下のMarkdownを`./marksites/`へ変換します。
69
+
70
+ ```sh
71
+ npx marksites
72
+ # npx marksites . marksites と同じ
73
+ ```
74
+
75
+ 単一Markdownの出力先を省略した場合も、`./marksites/`へ同じbasenameのHTMLを保存します。
76
+
77
+ フォルダを指定すると、配下の `.md` と `.markdown` を再帰的に変換します。出力先でも元のフォルダ階層を維持し、各ページの左側にファイル名で絞り込めるファイルツリー、本文上部に現在のファイルパスを示すパンくずを表示します。コメントが1件以上あるファイルには件数バッジを表示し、上位フォルダには配下の有効コメント合計件数を表示します。Markdownファイル間の相対リンクは `.html` へ書き換えられます。
78
+
79
+ ```sh
80
+ npx marksites docs public
81
+ ```
82
+
83
+ ディレクトリ変換の出力先を省略した場合は、カレントディレクトリの`marksites/`へ出力します。
84
+
85
+ ```sh
86
+ npx marksites docs
87
+ # ./marksites/ に出力
88
+ ```
89
+
90
+ プロジェクトルートを対象にする場合、`.git`、`node_modules`、`dist`、`coverage`は探索対象から除外されます。
91
+
92
+ 入力フォルダ配下の`.gitignore`を読み込み、Gitと同じパターン規則で除外されたファイルとフォルダを変換対象から外します。ネストした`.gitignore`と`!`による再包含にも対応します。また、`.marksites-build.json`を持つ既存の生成フォルダと、今回指定された出力フォルダは常に探索対象外です。
93
+
94
+ 2回目以降はMarkdown本文、コメント、ファイル構成、レンダー設定のSHA-256を比較し、影響したHTMLだけを再生成します。管理情報は出力先直下の `.marksites-build.json`、文書ごとのメタデータは各HTMLに隣接する `.<名前>.json` に保存されます。このJSONは再変換時にも上書きされません。現在はコメント情報を格納し、将来は文書固有のほかの情報も管理できるファイルとして扱います。
95
+
96
+ ### コメントを編集する
97
+
98
+ 軽量ローカルサーバーの起動中だけ、本文の選択範囲へコメントを追加・編集・削除できます。
99
+
100
+ 右サイドバーでは目次と現在ページのコメント一覧をタブで切り替えられます。タブにはコメント件数が表示され、コメントを選ぶと本文の対象箇所へ移動します。一覧先頭の追加ボタンから、範囲を選択しない文書全体へのコメントも作成できます。ヘッダーは固定され、各パネルは独立してスクロールします。
101
+
102
+ ```sh
103
+ npx marksites serve docs public/docs
104
+ # 通常は http://127.0.0.1:3000
105
+ ```
106
+
107
+ 入力・出力を省略して、カレントディレクトリを`./marksites/`へ変換・配信することもできます。
108
+
109
+ ```sh
110
+ npx marksites serve
111
+ # npx marksites serve . marksites と同じ
112
+ ```
113
+
114
+ `--host`、`--port`、`--open`を指定できます。ポートを省略すると3000を使用し、すでに使用中なら3001、3002の順に空きポートを自動選択します。`--port`を明示した場合は指定ポートだけを使用し、競合時はエラーになります。`--port 0`ではOSに空きポートを割り当てさせることもできます。実際のURLは起動時に表示され、`--open`もそのURLを開きます。
115
+
116
+ サーバーは起動時に差分変換を行い、アイドル中のファイル監視や定期処理は行いません。停止後もコメントはHTMLへ埋め込まれているため、生成HTMLを`file://`で閲覧できます。この場合、コメントは閲覧専用です。
117
+
118
+ `--open`はWindows、macOS、Linuxに加えてWSLからWindows側のブラウザを開く処理にも対応します。ブラウザ起動コマンドが見つからない場合もサーバーは停止せず、表示されたURLを手動で開けます。
119
+
120
+ 入力フォルダに`index.md`または`index.markdown`がある場合は、そのページを`/`で表示します。存在しない場合は、変換対象のファイルパスを並べたときの先頭ページを表示します。
121
+
122
+ 詳細な設計と運用上の注意は[ローカルサーバー設計](docs/local-server-design.md)を参照してください。
123
+
124
+ ### Webアプリの静的ファイルとして公開する
125
+
126
+ Next.jsやVue、ViteなどのWebアプリで公開する場合は、アプリの静的ファイル用ディレクトリ配下を出力先に指定します。
127
+
128
+ ```sh
129
+ npx marksites docs public/docs
130
+ ```
131
+
132
+ 例えば `docs/index.md` と `docs/guide.md` は、それぞれ `public/docs/index.html` と `public/docs/guide.html` に変換されます。Webアプリからは `/docs/index.html` と `/docs/guide.html` への通常のリンクとして参照できます。
133
+
134
+ Webアプリをビルドする前にHTMLを生成するよう、npmスクリプトへ追加できます。
135
+
136
+ ```json
137
+ {
138
+ "scripts": {
139
+ "build:docs": "marksites docs public/docs",
140
+ "build": "npm run build:docs && next build"
141
+ }
142
+ }
143
+ ```
144
+
145
+ Viteを使用する場合は、`next build` を `vite build` に置き換えます。
146
+
147
+ ## 開発
148
+
149
+ ```sh
150
+ npm test
151
+ ```
152
+
153
+ Markdownの解析には [marked](https://marked.js.org/)、表示スタイルには [github-markdown-css](https://github.com/sindresorhus/github-markdown-css)、コードのシンタックスハイライトには [highlight.js](https://highlightjs.org/) を使用しています。
@@ -0,0 +1,29 @@
1
+ export interface AnnotationSelection {
2
+ exact: string;
3
+ prefix: string;
4
+ suffix: string;
5
+ headingId: string | null;
6
+ startOffset: number;
7
+ endOffset: number;
8
+ }
9
+ export interface Annotation {
10
+ id: string;
11
+ selection: AnnotationSelection;
12
+ comment: {
13
+ body: string;
14
+ author: string | null;
15
+ };
16
+ status: "open" | "archived";
17
+ createdAt: string;
18
+ updatedAt: string;
19
+ }
20
+ export interface AnnotationDocument {
21
+ schemaVersion: 1;
22
+ document: string;
23
+ revision: number;
24
+ annotations: Annotation[];
25
+ }
26
+ export declare function emptyAnnotationDocument(document: string): AnnotationDocument;
27
+ export declare function countActiveAnnotations(data: AnnotationDocument): number;
28
+ export declare function validateAnnotationDocument(value: unknown, expectedDocument?: string): AnnotationDocument;
29
+ export declare function validateAnnotation(value: unknown): asserts value is Annotation;
@@ -0,0 +1,56 @@
1
+ export function emptyAnnotationDocument(document) {
2
+ return { schemaVersion: 1, document, revision: 0, annotations: [] };
3
+ }
4
+ export function countActiveAnnotations(data) {
5
+ return data.annotations.filter((annotation) => annotation.status === "open")
6
+ .length;
7
+ }
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ export function validateAnnotationDocument(value, expectedDocument) {
12
+ if (!isRecord(value) ||
13
+ value.schemaVersion !== 1 ||
14
+ typeof value.document !== "string" ||
15
+ !Number.isSafeInteger(value.revision) ||
16
+ Number(value.revision) < 0 ||
17
+ !Array.isArray(value.annotations)) {
18
+ throw new Error("Invalid annotation document");
19
+ }
20
+ if (expectedDocument !== undefined && value.document !== expectedDocument) {
21
+ throw new Error(`Annotation document path must be ${expectedDocument}`);
22
+ }
23
+ for (const item of value.annotations)
24
+ validateAnnotation(item);
25
+ return value;
26
+ }
27
+ export function validateAnnotation(value) {
28
+ if (isRecord(value) && value.status === "resolved") {
29
+ value.status = "archived";
30
+ }
31
+ if (!isRecord(value) ||
32
+ typeof value.id !== "string" ||
33
+ value.id.length === 0 ||
34
+ !isRecord(value.selection) ||
35
+ typeof value.selection.exact !== "string" ||
36
+ typeof value.selection.prefix !== "string" ||
37
+ typeof value.selection.suffix !== "string" ||
38
+ !(typeof value.selection.headingId === "string" ||
39
+ value.selection.headingId === null) ||
40
+ !Number.isSafeInteger(value.selection.startOffset) ||
41
+ !Number.isSafeInteger(value.selection.endOffset) ||
42
+ Number(value.selection.startOffset) < 0 ||
43
+ Number(value.selection.endOffset) < Number(value.selection.startOffset) ||
44
+ !isRecord(value.comment) ||
45
+ typeof value.comment.body !== "string" ||
46
+ !(typeof value.comment.author === "string" || value.comment.author === null) ||
47
+ (value.status !== "open" && value.status !== "archived") ||
48
+ typeof value.createdAt !== "string" ||
49
+ typeof value.updatedAt !== "string") {
50
+ throw new Error("Invalid annotation");
51
+ }
52
+ if (Number.isNaN(Date.parse(value.createdAt)) ||
53
+ Number.isNaN(Date.parse(value.updatedAt))) {
54
+ throw new Error("Invalid annotation timestamp");
55
+ }
56
+ }
@@ -0,0 +1,4 @@
1
+ import { type AnnotationDocument } from "./model.js";
2
+ export declare function serializeAnnotations(value: AnnotationDocument): string;
3
+ export declare function readAnnotations(path: string, document?: string): Promise<AnnotationDocument>;
4
+ export declare function writeAnnotations(path: string, value: AnnotationDocument): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { atomicWriteFile } from "../utils/files.js";
3
+ import { emptyAnnotationDocument, validateAnnotationDocument, } from "./model.js";
4
+ export function serializeAnnotations(value) {
5
+ return `${JSON.stringify(value, null, 2)}\n`;
6
+ }
7
+ export async function readAnnotations(path, document) {
8
+ let source;
9
+ try {
10
+ source = await readFile(path, "utf8");
11
+ }
12
+ catch (error) {
13
+ if (error.code === "ENOENT" &&
14
+ document !== undefined) {
15
+ const value = emptyAnnotationDocument(document);
16
+ await atomicWriteFile(path, serializeAnnotations(value));
17
+ return value;
18
+ }
19
+ throw error;
20
+ }
21
+ try {
22
+ return validateAnnotationDocument(JSON.parse(source), document);
23
+ }
24
+ catch (error) {
25
+ throw new Error(`Invalid annotation JSON at ${path}: ${error instanceof Error ? error.message : String(error)}`);
26
+ }
27
+ }
28
+ export async function writeAnnotations(path, value) {
29
+ validateAnnotationDocument(value);
30
+ await atomicWriteFile(path, serializeAnnotations(value));
31
+ }
@@ -0,0 +1,4 @@
1
+ export { convertDirectory, convertDirectoryDetailed, } from "../conversion/directory.js";
2
+ export { convertFile } from "../conversion/single-file.js";
3
+ export { BUILD_MANIFEST } from "../conversion/manifest.js";
4
+ export type { ConversionResult } from "../conversion/types.js";
@@ -0,0 +1,3 @@
1
+ export { convertDirectory, convertDirectoryDetailed, } from "../conversion/directory.js";
2
+ export { convertFile } from "../conversion/single-file.js";
3
+ export { BUILD_MANIFEST } from "../conversion/manifest.js";
@@ -0,0 +1,12 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ type SpawnBrowser = (command: string, args: string[], options: {
3
+ detached: true;
4
+ stdio: "ignore";
5
+ }) => ChildProcess;
6
+ interface BrowserOpenEnvironment {
7
+ platform?: NodeJS.Platform;
8
+ environment?: NodeJS.ProcessEnv;
9
+ spawnBrowser?: SpawnBrowser;
10
+ }
11
+ export declare function openBrowser(url: string, environment?: BrowserOpenEnvironment): Promise<boolean>;
12
+ export {};
@@ -0,0 +1,37 @@
1
+ import { spawn } from "node:child_process";
2
+ function browserCommands(url, platform, environment) {
3
+ if (platform === "win32") {
4
+ return [{ command: "cmd.exe", args: ["/d", "/s", "/c", "start", "", url] }];
5
+ }
6
+ if (platform === "darwin")
7
+ return [{ command: "open", args: [url] }];
8
+ const isWsl = Boolean(environment.WSL_DISTRO_NAME || environment.WSL_INTEROP);
9
+ return isWsl
10
+ ? [
11
+ { command: "cmd.exe", args: ["/d", "/s", "/c", "start", "", url] },
12
+ { command: "xdg-open", args: [url] },
13
+ ]
14
+ : [{ command: "xdg-open", args: [url] }];
15
+ }
16
+ function tryCommand(command, spawnBrowser) {
17
+ return new Promise((resolve) => {
18
+ const child = spawnBrowser(command.command, command.args, {
19
+ detached: true,
20
+ stdio: "ignore",
21
+ });
22
+ child.once("error", () => resolve(false));
23
+ child.once("spawn", () => {
24
+ child.unref();
25
+ resolve(true);
26
+ });
27
+ });
28
+ }
29
+ export async function openBrowser(url, environment = {}) {
30
+ const spawnBrowser = environment.spawnBrowser ?? spawn;
31
+ const commands = browserCommands(url, environment.platform ?? process.platform, environment.environment ?? process.env);
32
+ for (const command of commands) {
33
+ if (await tryCommand(command, spawnBrowser))
34
+ return true;
35
+ }
36
+ return false;
37
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { access, readFile, stat } from "node:fs/promises";
4
+ import { basename, join, resolve } from "node:path";
5
+ import { convertDirectoryDetailed } from "./conversion/directory.js";
6
+ import { convertFile } from "./conversion/single-file.js";
7
+ import { MARKSITES_RESERVED_PATH } from "./server/constants.js";
8
+ import { startMarksitesServer } from "./server/server.js";
9
+ import { openBrowser } from "./cli/open-browser.js";
10
+ function usage() {
11
+ console.error("Usage:\n marksites [input.md|input-directory] [output.html|output-directory]\n marksites serve [input-directory] [output-directory] [--host HOST] [--port PORT] [--open]");
12
+ process.exit(1);
13
+ }
14
+ function report(result) {
15
+ console.log(`Converted ${result.converted}; skipped ${result.skipped}; deleted ${result.deleted}; annotation files created ${result.annotationsCreated}; moved ${result.annotationsMoved}`);
16
+ for (const path of result.orphanedAnnotations)
17
+ console.warn(`Orphaned annotation JSON preserved: ${path}`);
18
+ }
19
+ async function serve(args) {
20
+ const positional = [];
21
+ let host = "127.0.0.1", port, shouldOpen = false;
22
+ for (let index = 0; index < args.length; index++) {
23
+ const argument = args[index];
24
+ if (argument === "--open") {
25
+ shouldOpen = true;
26
+ continue;
27
+ }
28
+ if (argument === "--host" || argument === "--port") {
29
+ const value = args[++index];
30
+ if (!value)
31
+ usage();
32
+ if (argument === "--host")
33
+ host = value;
34
+ else {
35
+ port = Number(value);
36
+ if (!Number.isInteger(port) || port < 0 || port > 65535)
37
+ throw new Error(`Invalid port: ${value}`);
38
+ }
39
+ continue;
40
+ }
41
+ if (argument.startsWith("--"))
42
+ throw new Error(`Unknown option: ${argument}`);
43
+ positional.push(argument);
44
+ }
45
+ if (positional.length > 2)
46
+ usage();
47
+ const inputArgument = positional[0] ?? ".";
48
+ const input = resolve(inputArgument);
49
+ if (!(await stat(input)).isDirectory())
50
+ throw new Error(`serve input must be a directory: ${inputArgument}`);
51
+ const initial = await convertDirectoryDetailed(input, positional[1]);
52
+ report(initial);
53
+ const output = initial.outputRoot;
54
+ try {
55
+ await access(join(output, MARKSITES_RESERVED_PATH));
56
+ throw new Error(`Reserved output path exists: ${MARKSITES_RESERVED_PATH}`);
57
+ }
58
+ catch (error) {
59
+ if (error.code !== "ENOENT")
60
+ throw error;
61
+ }
62
+ const manifest = JSON.parse(await readFile(join(output, ".marksites-build.json"), "utf8"));
63
+ const documents = new Map(Object.entries(manifest.files).map(([document, value]) => [
64
+ document,
65
+ value.annotations,
66
+ ]));
67
+ const projectId = createHash("sha256")
68
+ .update(input)
69
+ .digest("hex")
70
+ .slice(0, 16);
71
+ const outputs = Object.values(manifest.files)
72
+ .map((value) => value.output)
73
+ .sort((left, right) => left.localeCompare(right, "en"));
74
+ const entryPath = outputs.includes("index.html") ? "index.html" : outputs[0];
75
+ const server = await startMarksitesServer({
76
+ outputRoot: output,
77
+ entryPath,
78
+ host,
79
+ port,
80
+ fallbackPort: port === undefined,
81
+ projectId,
82
+ projectName: basename(input),
83
+ documents,
84
+ onAnnotationsChange: async () => {
85
+ await convertDirectoryDetailed(input, output);
86
+ },
87
+ });
88
+ console.log(`marksites server: ${server.url}`);
89
+ if (shouldOpen) {
90
+ const opened = await openBrowser(server.url);
91
+ if (!opened) {
92
+ console.warn(`Could not open a browser automatically. Open ${server.url} manually.`);
93
+ }
94
+ }
95
+ await new Promise((done, fail) => {
96
+ let closing = false;
97
+ const close = () => {
98
+ if (closing)
99
+ return;
100
+ closing = true;
101
+ server.close().then(done, fail);
102
+ };
103
+ process.once("SIGINT", close);
104
+ process.once("SIGTERM", close);
105
+ });
106
+ }
107
+ async function main() {
108
+ if (process.argv[2] === "serve")
109
+ return serve(process.argv.slice(3));
110
+ const inputArgument = process.argv[2] ?? ".";
111
+ const input = resolve(inputArgument);
112
+ const inputStat = await stat(input);
113
+ const outputArgument = process.argv[3];
114
+ if (inputStat.isDirectory()) {
115
+ report(await convertDirectoryDetailed(input, outputArgument));
116
+ return;
117
+ }
118
+ if (!inputStat.isFile())
119
+ throw new Error(`Input is not a file or directory: ${inputArgument}`);
120
+ console.log(`Created ${await convertFile(input, outputArgument)}`);
121
+ }
122
+ main().catch((error) => {
123
+ console.error(error instanceof Error ? error.message : String(error));
124
+ process.exitCode = 1;
125
+ });
@@ -0,0 +1,3 @@
1
+ import type { ConversionResult } from "./types.js";
2
+ export declare function convertDirectoryDetailed(input: string, outputArgument?: string): Promise<ConversionResult>;
3
+ export declare function convertDirectory(input: string, outputArgument?: string): Promise<number>;