marksites 0.1.0 → 0.2.2
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 +18 -5
- package/dist/cli.js +64 -6
- package/dist/conversion/assets.d.ts +8 -0
- package/dist/conversion/assets.js +62 -0
- package/dist/conversion/directory.d.ts +2 -2
- package/dist/conversion/directory.js +35 -6
- package/dist/conversion/navigation.js +2 -0
- package/dist/conversion/paths.d.ts +1 -0
- package/dist/conversion/paths.js +23 -0
- package/dist/conversion/rendering.d.ts +1 -1
- package/dist/conversion/rendering.js +4 -2
- package/dist/conversion/single-file.js +5 -0
- package/dist/conversion/types.d.ts +8 -0
- package/dist/conversion/watch.d.ts +7 -0
- package/dist/conversion/watch.js +69 -0
- package/dist/features/file-tree/index.d.ts +1 -1
- package/dist/features/file-tree/index.js +297 -43
- package/dist/features/header/index.d.ts +7 -1
- package/dist/features/header/index.js +17 -9
- package/dist/features/image-viewer/index.d.ts +6 -0
- package/dist/features/image-viewer/index.js +28 -0
- package/dist/features/sidebar/index.js +8 -6
- package/dist/features/table-of-contents/index.js +9 -4
- package/dist/markdown-to-html.js +19 -10
- package/dist/server/html-security.js +2 -0
- package/dist/server/static-files.js +5 -0
- package/dist/template/document.d.ts +0 -2
- package/dist/template/document.js +3 -5
- package/dist/template/styles.d.ts +2 -2
- package/dist/template/styles.js +30 -3
- package/dist/types.d.ts +4 -0
- package/dist/utils/files.d.ts +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -25,11 +25,11 @@ const html = markdownToHtml(markdown, {
|
|
|
25
25
|
});
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
見出しレベル2から6までの目次と、GitHub互換の見出しアンカーが自動生成されます。ファイルツリーはGitHubのFiles
|
|
28
|
+
見出しレベル2から6までの目次と、GitHub互換の見出しアンカーが自動生成されます。ファイルツリーはGitHubのFilesペインと同様、画面左端へ接する全高のサイドバーとして常時表示し、本文を中央、`目次`とコメントを画面右側へ配置します。ファイル一覧は階層ツリーと更新順を切り替えられ、更新順ではMarkdownのローカルファイル更新日時(mtime)を新しい順に日付でまとめ、時刻を左側の固定幅列、濃い色・太字のファイル名とフォルダアイコン付きの親ディレクトリをその右の2段表示にします。親ディレクトリは変換ルートを`/`、配下を`/src/`のように表示します。同じ日付で同一フォルダのファイルが2件以上連続する場合は、ルート直下を含め、更新順を変えずにフォルダ見出しへまとめます。日付見出しではファイル件数を確認でき、クリックで折り畳めます。初期状態では最新日と現在ファイルを含む日を開き、検索中は一致する日を一時的に展開します。完全な相対パスは各行のツールチップでも確認できます。左サイドバーはヘッダーの四角いペインアイコンで折り畳むことができ、閉じると本文が左へ広がります。閉じた場合はページ上部に同形の復帰アイコンと、従来どおりポップオーバーでファイル一覧を開く`ファイル`ボタンを並べます。サイドバーの閉状態は`file-sidebar=closed`、ポップオーバーは`marksites-files=open`、更新順表示は`file-view=recent`、開いたフォルダは短縮IDを値とする`open` GETパラメータでページ間に引き継がれます。ファイルパス直後のアイコンでパスをコピーできます。Markdownの更新日時は本文カード上部で、ブラウザのローカルタイムゾーンを使った`YYYY-MM-DD HH:mm:ss`形式にして表示します。タイムゾーン名やUTCオフセットは表示しません。JavaScript実行前はUTCの日時を同じ形式で表示します。現在位置に対応する見出しもハイライトされます。狭い画面では右サイドバーを本文上部、ファイルサイドバーを必要なときだけ開く固定オーバーレイとして表示します。
|
|
29
29
|
|
|
30
|
-
左ファイルサイドバーは幅280px
|
|
30
|
+
左ファイルサイドバーは幅280pxとし、開閉アイコンは枠線なしで表示します。更新日時はヘッダーのファイルパス右隣へ細い縦線で区切って表示し、狭い画面では省略します。
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
画面上端には固定ヘッダーを表示します。濃色背景と白文字の`marksites`ロゴ、その右に現在のファイルパスを表示します。ファイルパスから開くポップオーバーは左ファイルサイドバーを閉じている場合だけ利用できます。ブラウザのタブ名は`marksites | <ファイル名>`形式です。右上の月・太陽アイコンでライト/ダークモードを切り替え、地球アイコンで生成UIの日本語/英語を切り替えます。Markdown本文、見出し、ファイル名、コメント本文は翻訳対象にしません。選択状態は`theme`と`lang` GETパラメータでページ間に引き継がれます。
|
|
33
33
|
|
|
34
34
|
```ts
|
|
35
35
|
const html = markdownToHtml(markdown, {
|
|
@@ -74,7 +74,9 @@ npx marksites
|
|
|
74
74
|
|
|
75
75
|
単一Markdownの出力先を省略した場合も、`./marksites/`へ同じbasenameのHTMLを保存します。
|
|
76
76
|
|
|
77
|
-
フォルダを指定すると、配下の `.md` と `.markdown`
|
|
77
|
+
フォルダを指定すると、配下の `.md` と `.markdown` を再帰的に変換します。出力先でも元のフォルダ階層を維持し、各ページの左側にファイル名で絞り込めるファイルツリーと相対パスで検索できる更新順一覧、固定ヘッダーに現在のファイルパスを示すパンくずを表示します。コメントが1件以上あるファイルには件数バッジを表示し、上位フォルダには配下の有効コメント合計件数を表示します。Markdownファイル間の相対リンクは `.html` へ書き換えられます。
|
|
78
|
+
|
|
79
|
+
相対パスで参照したローカル画像は、内容ハッシュ付きの名前で出力先の`_marksites-assets/`へコピーされます。同名画像の衝突を避け、画像変更時は参照するHTMLだけを更新します。外部URL、data URL、ルート相対URLはそのまま保持します。本文画像をクリックすると全画面ビューアーが開き、ボタンまたはホイールで拡大・縮小でき、ドラッグで位置を移動できます。Escapeまたは右上の閉じるボタンで閉じます。
|
|
78
80
|
|
|
79
81
|
```sh
|
|
80
82
|
npx marksites docs public
|
|
@@ -113,7 +115,16 @@ npx marksites serve
|
|
|
113
115
|
|
|
114
116
|
`--host`、`--port`、`--open`を指定できます。ポートを省略すると3000を使用し、すでに使用中なら3001、3002の順に空きポートを自動選択します。`--port`を明示した場合は指定ポートだけを使用し、競合時はエラーになります。`--port 0`ではOSに空きポートを割り当てさせることもできます。実際のURLは起動時に表示され、`--open`もそのURLを開きます。
|
|
115
117
|
|
|
116
|
-
|
|
118
|
+
ディレクトリ変換または`serve`へ`--watch`を付けると、Markdownや参照画像の変更を検知して差分変換します。Markdownの新規追加や削除、新しいサブディレクトリにも対応します。
|
|
119
|
+
|
|
120
|
+
`--verbose`を付けると、文書ごとの変換・スキップ結果を表示します。`--watch`と併用した場合は、検知したイベント種別・対象パスと再変換の開始も表示します。
|
|
121
|
+
|
|
122
|
+
```sh
|
|
123
|
+
npx marksites docs public/docs --watch
|
|
124
|
+
npx marksites serve docs public/docs --port 4000 --watch --verbose
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
サーバーは起動時に差分変換を行い、`--watch`指定時だけイベント駆動で入力を監視します。ポーリングや常駐文書キャッシュは使用しません。停止後もコメントはHTMLへ埋め込まれているため、生成HTMLを`file://`で閲覧できます。この場合、コメントは閲覧専用です。
|
|
117
128
|
|
|
118
129
|
`--open`はWindows、macOS、Linuxに加えてWSLからWindows側のブラウザを開く処理にも対応します。ブラウザ起動コマンドが見つからない場合もサーバーは停止せず、表示されたURLを手動で開けます。
|
|
119
130
|
|
|
@@ -146,6 +157,8 @@ Viteを使用する場合は、`next build` を `vite build` に置き換えま
|
|
|
146
157
|
|
|
147
158
|
## 開発
|
|
148
159
|
|
|
160
|
+
依存パッケージは、未確認の新バージョンを自動取得しないよう`latest`を使用せず、検証済みバージョンを`package.json`と`package-lock.json`へ記録します。更新時はバージョンを明示的に変更し、テストを実行します。
|
|
161
|
+
|
|
149
162
|
```sh
|
|
150
163
|
npm test
|
|
151
164
|
```
|
package/dist/cli.js
CHANGED
|
@@ -7,8 +7,9 @@ import { convertFile } from "./conversion/single-file.js";
|
|
|
7
7
|
import { MARKSITES_RESERVED_PATH } from "./server/constants.js";
|
|
8
8
|
import { startMarksitesServer } from "./server/server.js";
|
|
9
9
|
import { openBrowser } from "./cli/open-browser.js";
|
|
10
|
+
import { watchConversionInput } from "./conversion/watch.js";
|
|
10
11
|
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
|
+
console.error("Usage:\n marksites [input.md|input-directory] [output.html|output-directory] [--watch] [--verbose]\n marksites serve [input-directory] [output-directory] [--host HOST] [--port PORT] [--open] [--watch] [--verbose]");
|
|
12
13
|
process.exit(1);
|
|
13
14
|
}
|
|
14
15
|
function report(result) {
|
|
@@ -19,12 +20,22 @@ function report(result) {
|
|
|
19
20
|
async function serve(args) {
|
|
20
21
|
const positional = [];
|
|
21
22
|
let host = "127.0.0.1", port, shouldOpen = false;
|
|
23
|
+
let shouldWatch = false;
|
|
24
|
+
let verbose = false;
|
|
22
25
|
for (let index = 0; index < args.length; index++) {
|
|
23
26
|
const argument = args[index];
|
|
24
27
|
if (argument === "--open") {
|
|
25
28
|
shouldOpen = true;
|
|
26
29
|
continue;
|
|
27
30
|
}
|
|
31
|
+
if (argument === "--watch") {
|
|
32
|
+
shouldWatch = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (argument === "--verbose") {
|
|
36
|
+
verbose = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
28
39
|
if (argument === "--host" || argument === "--port") {
|
|
29
40
|
const value = args[++index];
|
|
30
41
|
if (!value)
|
|
@@ -48,7 +59,10 @@ async function serve(args) {
|
|
|
48
59
|
const input = resolve(inputArgument);
|
|
49
60
|
if (!(await stat(input)).isDirectory())
|
|
50
61
|
throw new Error(`serve input must be a directory: ${inputArgument}`);
|
|
51
|
-
const
|
|
62
|
+
const conversionOptions = {
|
|
63
|
+
onLog: verbose ? (message) => console.log(message) : undefined,
|
|
64
|
+
};
|
|
65
|
+
const initial = await convertDirectoryDetailed(input, positional[1], conversionOptions);
|
|
52
66
|
report(initial);
|
|
53
67
|
const output = initial.outputRoot;
|
|
54
68
|
try {
|
|
@@ -82,10 +96,22 @@ async function serve(args) {
|
|
|
82
96
|
projectName: basename(input),
|
|
83
97
|
documents,
|
|
84
98
|
onAnnotationsChange: async () => {
|
|
85
|
-
await convertDirectoryDetailed(input, output);
|
|
99
|
+
await convertDirectoryDetailed(input, output, conversionOptions);
|
|
86
100
|
},
|
|
87
101
|
});
|
|
88
102
|
console.log(`marksites server: ${server.url}`);
|
|
103
|
+
const watcher = shouldWatch
|
|
104
|
+
? await watchConversionInput(input, output, async () => {
|
|
105
|
+
const result = await convertDirectoryDetailed(input, output, conversionOptions);
|
|
106
|
+
report(result);
|
|
107
|
+
const nextManifest = JSON.parse(await readFile(join(output, ".marksites-build.json"), "utf8"));
|
|
108
|
+
documents.clear();
|
|
109
|
+
for (const [document, value] of Object.entries(nextManifest.files))
|
|
110
|
+
documents.set(document, value.annotations);
|
|
111
|
+
}, conversionOptions)
|
|
112
|
+
: undefined;
|
|
113
|
+
if (watcher)
|
|
114
|
+
console.log(`Watching ${input}`);
|
|
89
115
|
if (shouldOpen) {
|
|
90
116
|
const opened = await openBrowser(server.url);
|
|
91
117
|
if (!opened) {
|
|
@@ -98,6 +124,7 @@ async function serve(args) {
|
|
|
98
124
|
if (closing)
|
|
99
125
|
return;
|
|
100
126
|
closing = true;
|
|
127
|
+
watcher?.close();
|
|
101
128
|
server.close().then(done, fail);
|
|
102
129
|
};
|
|
103
130
|
process.once("SIGINT", close);
|
|
@@ -107,16 +134,47 @@ async function serve(args) {
|
|
|
107
134
|
async function main() {
|
|
108
135
|
if (process.argv[2] === "serve")
|
|
109
136
|
return serve(process.argv.slice(3));
|
|
110
|
-
const
|
|
137
|
+
const args = process.argv.slice(2);
|
|
138
|
+
const shouldWatch = args.includes("--watch");
|
|
139
|
+
const verbose = args.includes("--verbose");
|
|
140
|
+
const positional = args.filter((argument) => argument !== "--watch" && argument !== "--verbose");
|
|
141
|
+
const unknown = positional.find((argument) => argument.startsWith("--"));
|
|
142
|
+
if (unknown)
|
|
143
|
+
throw new Error(`Unknown option: ${unknown}`);
|
|
144
|
+
if (positional.length > 2)
|
|
145
|
+
usage();
|
|
146
|
+
const inputArgument = positional[0] ?? ".";
|
|
111
147
|
const input = resolve(inputArgument);
|
|
112
148
|
const inputStat = await stat(input);
|
|
113
|
-
const outputArgument =
|
|
149
|
+
const outputArgument = positional[1];
|
|
150
|
+
const conversionOptions = {
|
|
151
|
+
onLog: verbose ? (message) => console.log(message) : undefined,
|
|
152
|
+
};
|
|
114
153
|
if (inputStat.isDirectory()) {
|
|
115
|
-
|
|
154
|
+
const initial = await convertDirectoryDetailed(input, outputArgument, conversionOptions);
|
|
155
|
+
report(initial);
|
|
156
|
+
if (shouldWatch) {
|
|
157
|
+
const watcher = await watchConversionInput(input, initial.outputRoot, async () => {
|
|
158
|
+
report(await convertDirectoryDetailed(input, initial.outputRoot, conversionOptions));
|
|
159
|
+
}, conversionOptions);
|
|
160
|
+
console.log(`Watching ${input}`);
|
|
161
|
+
await new Promise((done) => {
|
|
162
|
+
const close = () => {
|
|
163
|
+
watcher.close();
|
|
164
|
+
done();
|
|
165
|
+
};
|
|
166
|
+
process.once("SIGINT", close);
|
|
167
|
+
process.once("SIGTERM", close);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
116
170
|
return;
|
|
117
171
|
}
|
|
118
172
|
if (!inputStat.isFile())
|
|
119
173
|
throw new Error(`Input is not a file or directory: ${inputArgument}`);
|
|
174
|
+
if (shouldWatch)
|
|
175
|
+
throw new Error("--watch input must be a directory");
|
|
176
|
+
if (verbose)
|
|
177
|
+
console.log(`Converting ${input}`);
|
|
120
178
|
console.log(`Created ${await convertFile(input, outputArgument)}`);
|
|
121
179
|
}
|
|
122
180
|
main().catch((error) => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Token } from "marked";
|
|
2
|
+
export declare const ASSET_DIRECTORY = "_marksites-assets";
|
|
3
|
+
export interface PreparedAssets {
|
|
4
|
+
hash: string;
|
|
5
|
+
outputs: string[];
|
|
6
|
+
rewrite(token: Token): void;
|
|
7
|
+
}
|
|
8
|
+
export declare function prepareImageAssets(markdown: string, sourcePath: string, outputPath: string, outputRoot: string): Promise<PreparedAssets>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, extname, join, posix, resolve } from "node:path";
|
|
3
|
+
import { marked } from "marked";
|
|
4
|
+
import { atomicWriteFile } from "../utils/files.js";
|
|
5
|
+
import { contentHash } from "./rendering.js";
|
|
6
|
+
import { pathExists } from "./paths.js";
|
|
7
|
+
export const ASSET_DIRECTORY = "_marksites-assets";
|
|
8
|
+
function localPath(href, sourcePath) {
|
|
9
|
+
const raw = href.split(/[?#]/, 1)[0];
|
|
10
|
+
if (!raw ||
|
|
11
|
+
raw.startsWith("/") ||
|
|
12
|
+
raw.startsWith("//") ||
|
|
13
|
+
/^[a-z][a-z\d+.-]*:/i.test(raw))
|
|
14
|
+
return undefined;
|
|
15
|
+
try {
|
|
16
|
+
return resolve(dirname(sourcePath), decodeURIComponent(raw));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function prepareImageAssets(markdown, sourcePath, outputPath, outputRoot) {
|
|
23
|
+
const hrefs = new Set();
|
|
24
|
+
marked.walkTokens(marked.lexer(markdown), (token) => {
|
|
25
|
+
if (token.type === "image")
|
|
26
|
+
hrefs.add(token.href);
|
|
27
|
+
});
|
|
28
|
+
const rewrites = new Map();
|
|
29
|
+
const fingerprints = [];
|
|
30
|
+
const outputs = new Set();
|
|
31
|
+
for (const href of [...hrefs].sort()) {
|
|
32
|
+
const source = localPath(href, sourcePath);
|
|
33
|
+
if (!source || !(await pathExists(source)))
|
|
34
|
+
continue;
|
|
35
|
+
let data;
|
|
36
|
+
try {
|
|
37
|
+
data = await readFile(source);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const digest = contentHash(data).slice("sha256:".length);
|
|
43
|
+
const extension = extname(source).toLowerCase().replace(/[^.a-z0-9]/g, "");
|
|
44
|
+
const assetPath = posix.join(ASSET_DIRECTORY, `${digest}${extension}`);
|
|
45
|
+
const suffix = href.slice(href.split(/[?#]/, 1)[0].length);
|
|
46
|
+
const relativeHref = posix.relative(posix.dirname(outputPath), assetPath);
|
|
47
|
+
rewrites.set(href, `${relativeHref || posix.basename(assetPath)}${suffix}`);
|
|
48
|
+
fingerprints.push(`${href}\0${digest}`);
|
|
49
|
+
outputs.add(assetPath);
|
|
50
|
+
const destination = join(outputRoot, ...assetPath.split("/"));
|
|
51
|
+
if (!(await pathExists(destination)))
|
|
52
|
+
await atomicWriteFile(destination, data);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
hash: contentHash(fingerprints.join("\n")),
|
|
56
|
+
outputs: [...outputs].sort(),
|
|
57
|
+
rewrite(token) {
|
|
58
|
+
if (token.type === "image")
|
|
59
|
+
token.href = rewrites.get(token.href) ?? token.href;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ConversionResult } from "./types.js";
|
|
2
|
-
export declare function convertDirectoryDetailed(input: string, outputArgument?: string): Promise<ConversionResult>;
|
|
1
|
+
import type { ConversionResult, ConversionOptions } from "./types.js";
|
|
2
|
+
export declare function convertDirectoryDetailed(input: string, outputArgument?: string, options?: ConversionOptions): Promise<ConversionResult>;
|
|
3
3
|
export declare function convertDirectory(input: string, outputArgument?: string): Promise<number>;
|
|
@@ -8,6 +8,7 @@ import { BUILD_MANIFEST, assertNoOutputCollisions, loadManifest, writeManifest,
|
|
|
8
8
|
import { buildBreadcrumbs, buildFileTree } from "./navigation.js";
|
|
9
9
|
import { DEFAULT_OUTPUT_DIRECTORY, findMarkdownFiles, firstExistingPath, pathExists, toLegacyMetadataPaths, } from "./paths.js";
|
|
10
10
|
import { OUTPUT_COMPATIBILITY_VERSION, contentHash, renderFingerprint, rewriteMarkdownLinks, } from "./rendering.js";
|
|
11
|
+
import { prepareImageAssets } from "./assets.js";
|
|
11
12
|
async function migrateLegacyMetadata(files, output) {
|
|
12
13
|
let moved = 0;
|
|
13
14
|
for (const file of files) {
|
|
@@ -34,6 +35,22 @@ async function loadSources(files) {
|
|
|
34
35
|
file.modifiedAt = sourceStat.mtime.toISOString();
|
|
35
36
|
}
|
|
36
37
|
}
|
|
38
|
+
async function prepareAssets(files, output) {
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
const assets = await prepareImageAssets(file.source, file.sourcePath, file.outputPath, output);
|
|
41
|
+
file.assetHash = assets.hash;
|
|
42
|
+
file.assetOutputs = assets.outputs;
|
|
43
|
+
file.rewriteImages = assets.rewrite;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function removeUnusedAssets(previous, files, output) {
|
|
47
|
+
const current = new Set(files.flatMap((file) => file.assetOutputs ?? []));
|
|
48
|
+
const old = new Set(Object.values(previous?.files ?? {}).flatMap((file) => file.assets ?? []));
|
|
49
|
+
for (const asset of old) {
|
|
50
|
+
if (!current.has(asset))
|
|
51
|
+
await rm(join(output, ...asset.split("/")), { force: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
37
54
|
async function moveRenamedDocuments(files, previous, output, removed) {
|
|
38
55
|
let deleted = 0;
|
|
39
56
|
let metadataMoved = 0;
|
|
@@ -104,7 +121,7 @@ async function removeDeletedHtml(removed, previous, output) {
|
|
|
104
121
|
}
|
|
105
122
|
return { deleted, orphaned };
|
|
106
123
|
}
|
|
107
|
-
async function renderChangedFiles(files, input, output, previous, full) {
|
|
124
|
+
async function renderChangedFiles(files, input, output, previous, full, options) {
|
|
108
125
|
let converted = 0;
|
|
109
126
|
let skipped = 0;
|
|
110
127
|
const manifestFiles = {};
|
|
@@ -116,6 +133,7 @@ async function renderChangedFiles(files, input, output, previous, full) {
|
|
|
116
133
|
old.sourceHash !== file.sourceHash ||
|
|
117
134
|
old.modifiedAt !== file.modifiedAt ||
|
|
118
135
|
old.annotationHash !== file.annotationHash ||
|
|
136
|
+
old.assetHash !== file.assetHash ||
|
|
119
137
|
!(await pathExists(destination));
|
|
120
138
|
if (changed) {
|
|
121
139
|
await atomicWriteFile(destination, renderMarkdown(file.source, {
|
|
@@ -126,12 +144,19 @@ async function renderChangedFiles(files, input, output, previous, full) {
|
|
|
126
144
|
items: buildFileTree(files, file.outputPath),
|
|
127
145
|
breadcrumbs: buildBreadcrumbs(files, file),
|
|
128
146
|
},
|
|
129
|
-
markedOptions: {
|
|
147
|
+
markedOptions: {
|
|
148
|
+
walkTokens: (token) => {
|
|
149
|
+
rewriteMarkdownLinks(token);
|
|
150
|
+
file.rewriteImages?.(token);
|
|
151
|
+
},
|
|
152
|
+
},
|
|
130
153
|
}, file.annotations));
|
|
131
154
|
converted++;
|
|
155
|
+
options.onLog?.(`Converted ${file.relativePath} -> ${file.outputPath}`);
|
|
132
156
|
}
|
|
133
157
|
else {
|
|
134
158
|
skipped++;
|
|
159
|
+
options.onLog?.(`Skipped ${file.relativePath}`);
|
|
135
160
|
}
|
|
136
161
|
manifestFiles[file.relativePath] = {
|
|
137
162
|
sourceHash: file.sourceHash,
|
|
@@ -139,11 +164,13 @@ async function renderChangedFiles(files, input, output, previous, full) {
|
|
|
139
164
|
annotationHash: file.annotationHash,
|
|
140
165
|
output: file.outputPath,
|
|
141
166
|
annotations: file.metadataPath,
|
|
167
|
+
assetHash: file.assetHash,
|
|
168
|
+
assets: file.assetOutputs,
|
|
142
169
|
};
|
|
143
170
|
}
|
|
144
171
|
return { converted, skipped, files: manifestFiles };
|
|
145
172
|
}
|
|
146
|
-
export async function convertDirectoryDetailed(input, outputArgument) {
|
|
173
|
+
export async function convertDirectoryDetailed(input, outputArgument, options = {}) {
|
|
147
174
|
const output = outputArgument
|
|
148
175
|
? resolve(outputArgument)
|
|
149
176
|
: resolve(DEFAULT_OUTPUT_DIRECTORY);
|
|
@@ -163,6 +190,7 @@ export async function convertDirectoryDetailed(input, outputArgument) {
|
|
|
163
190
|
console.warn(loaded.warning);
|
|
164
191
|
const previous = loaded.manifest;
|
|
165
192
|
await loadSources(files);
|
|
193
|
+
await prepareAssets(files, output);
|
|
166
194
|
let annotationsMoved = await migrateLegacyMetadata(files, output);
|
|
167
195
|
const currentPaths = new Set(files.map((file) => file.relativePath));
|
|
168
196
|
const removed = Object.keys(previous?.files ?? {}).filter((path) => !currentPaths.has(path));
|
|
@@ -170,7 +198,7 @@ export async function convertDirectoryDetailed(input, outputArgument) {
|
|
|
170
198
|
annotationsMoved += renamed.metadataMoved;
|
|
171
199
|
const annotationsCreated = await loadMetadata(files, output);
|
|
172
200
|
const treeHash = contentHash(files
|
|
173
|
-
.map((file) => `${file.relativePath}\0${file.annotations ? countActiveAnnotations(file.annotations) : 0}`)
|
|
201
|
+
.map((file) => `${file.relativePath}\0${file.modifiedAt}\0${file.annotations ? countActiveAnnotations(file.annotations) : 0}`)
|
|
174
202
|
.sort()
|
|
175
203
|
.join("\n"));
|
|
176
204
|
const fingerprint = renderFingerprint();
|
|
@@ -181,12 +209,13 @@ export async function convertDirectoryDetailed(input, outputArgument) {
|
|
|
181
209
|
previous.generator.renderFingerprint !== fingerprint ||
|
|
182
210
|
previous.treeHash !== treeHash;
|
|
183
211
|
const removedResult = await removeDeletedHtml(removed, previous, output);
|
|
184
|
-
const rendered = await renderChangedFiles(files, input, output, previous, full);
|
|
212
|
+
const rendered = await renderChangedFiles(files, input, output, previous, full, options);
|
|
213
|
+
await removeUnusedAssets(previous, files, output);
|
|
185
214
|
await writeManifest(manifestPath, {
|
|
186
215
|
schemaVersion: 1,
|
|
187
216
|
generator: {
|
|
188
217
|
name: "marksites",
|
|
189
|
-
version: "0.
|
|
218
|
+
version: "0.2.0",
|
|
190
219
|
outputCompatibilityVersion: OUTPUT_COMPATIBILITY_VERSION,
|
|
191
220
|
renderFingerprint: fingerprint,
|
|
192
221
|
},
|
|
@@ -33,6 +33,8 @@ export function buildFileTree(files, currentOutputPath) {
|
|
|
33
33
|
.map((file) => ({
|
|
34
34
|
type: "file",
|
|
35
35
|
name: basename(file.relativePath),
|
|
36
|
+
path: file.relativePath,
|
|
37
|
+
modifiedAt: file.modifiedAt,
|
|
36
38
|
href: encodeRelativeHref(posix.relative(posix.dirname(currentOutputPath), file.outputPath) ||
|
|
37
39
|
posix.basename(file.outputPath)),
|
|
38
40
|
current: file.outputPath === currentOutputPath,
|
|
@@ -8,3 +8,4 @@ export declare function toPosix(path: string): string;
|
|
|
8
8
|
export declare function pathExists(path: string): Promise<boolean>;
|
|
9
9
|
export declare function firstExistingPath(paths: string[]): Promise<string | undefined>;
|
|
10
10
|
export declare function findMarkdownFiles(root: string, excludedDirectory?: string): Promise<MarkdownFile[]>;
|
|
11
|
+
export declare function findWatchDirectories(root: string, excludedDirectory?: string): Promise<string[]>;
|
package/dist/conversion/paths.js
CHANGED
|
@@ -84,3 +84,26 @@ export async function findMarkdownFiles(root, excludedDirectory) {
|
|
|
84
84
|
await visit(root, []);
|
|
85
85
|
return files;
|
|
86
86
|
}
|
|
87
|
+
export async function findWatchDirectories(root, excludedDirectory) {
|
|
88
|
+
const directories = [];
|
|
89
|
+
async function visit(directory, inheritedRules) {
|
|
90
|
+
if (directory !== root && (await pathExists(join(directory, BUILD_MANIFEST))))
|
|
91
|
+
return;
|
|
92
|
+
directories.push(directory);
|
|
93
|
+
const rules = await loadGitignoreRules(root, directory, inheritedRules);
|
|
94
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (!entry.isDirectory())
|
|
97
|
+
continue;
|
|
98
|
+
const absolute = join(directory, entry.name);
|
|
99
|
+
const relativePath = toPosix(relative(root, absolute));
|
|
100
|
+
if (resolve(absolute) === excludedDirectory ||
|
|
101
|
+
EXCLUDED_DIRECTORY_NAMES.has(entry.name) ||
|
|
102
|
+
isGitignored(relativePath, true, rules))
|
|
103
|
+
continue;
|
|
104
|
+
await visit(absolute, rules);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
await visit(root, []);
|
|
108
|
+
return directories;
|
|
109
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Token } from "marked";
|
|
2
|
-
export declare const OUTPUT_COMPATIBILITY_VERSION =
|
|
2
|
+
export declare const OUTPUT_COMPATIBILITY_VERSION = 7;
|
|
3
3
|
export declare function contentHash(value: string | Buffer): string;
|
|
4
4
|
export declare function rewriteMarkdownLinks(token: Token): void;
|
|
5
5
|
export declare function renderFingerprint(): string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { emptyAnnotationDocument } from "../annotations/model.js";
|
|
3
3
|
import { renderMarkdown } from "../markdown-to-html.js";
|
|
4
|
-
export const OUTPUT_COMPATIBILITY_VERSION =
|
|
4
|
+
export const OUTPUT_COMPATIBILITY_VERSION = 7;
|
|
5
5
|
export function contentHash(value) {
|
|
6
6
|
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
7
7
|
}
|
|
@@ -18,7 +18,7 @@ export function rewriteMarkdownLinks(token) {
|
|
|
18
18
|
token.href = rewriteMarkdownHref(token.href);
|
|
19
19
|
}
|
|
20
20
|
export function renderFingerprint() {
|
|
21
|
-
const representativeHtml = renderMarkdown("## Heading\n\n[Document](guide.md)\n\n```js\nconst value = 1;\n```\n", {
|
|
21
|
+
const representativeHtml = renderMarkdown("## Heading\n\n[Document](guide.md)\n\n\n\n```js\nconst value = 1;\n```\n", {
|
|
22
22
|
title: "marksites-render-fingerprint",
|
|
23
23
|
modifiedAt: "2026-01-01T00:00:00.000Z",
|
|
24
24
|
fileTree: {
|
|
@@ -27,8 +27,10 @@ export function renderFingerprint() {
|
|
|
27
27
|
{
|
|
28
28
|
type: "file",
|
|
29
29
|
name: "index.md",
|
|
30
|
+
path: "index.md",
|
|
30
31
|
href: "index.html",
|
|
31
32
|
current: true,
|
|
33
|
+
modifiedAt: "2026-01-01T00:00:00.000Z",
|
|
32
34
|
},
|
|
33
35
|
],
|
|
34
36
|
breadcrumbs: [{ name: "docs" }, { name: "index.md", current: true }],
|
|
@@ -4,6 +4,7 @@ import { readAnnotations } from "../annotations/storage.js";
|
|
|
4
4
|
import { renderMarkdown } from "../markdown-to-html.js";
|
|
5
5
|
import { atomicWriteFile } from "../utils/files.js";
|
|
6
6
|
import { DEFAULT_OUTPUT_DIRECTORY, firstExistingPath, isMarkdown, pathExists, } from "./paths.js";
|
|
7
|
+
import { prepareImageAssets } from "./assets.js";
|
|
7
8
|
export async function convertFile(input, outputArgument) {
|
|
8
9
|
if (!isMarkdown(input))
|
|
9
10
|
throw new Error(`Input file must use .md or .markdown: ${input}`);
|
|
@@ -27,9 +28,13 @@ export async function convertFile(input, outputArgument) {
|
|
|
27
28
|
stat(input),
|
|
28
29
|
]);
|
|
29
30
|
await mkdir(dirname(output), { recursive: true });
|
|
31
|
+
const assets = await prepareImageAssets(markdown, input, basename(output), dirname(output));
|
|
30
32
|
await atomicWriteFile(output, renderMarkdown(markdown, {
|
|
31
33
|
title: basename(input, extname(input)),
|
|
32
34
|
modifiedAt: sourceStat.mtime.toISOString(),
|
|
35
|
+
markedOptions: {
|
|
36
|
+
walkTokens: assets.rewrite,
|
|
37
|
+
},
|
|
33
38
|
}, annotations));
|
|
34
39
|
return output;
|
|
35
40
|
}
|
|
@@ -9,6 +9,9 @@ export interface MarkdownFile {
|
|
|
9
9
|
modifiedAt?: string;
|
|
10
10
|
annotations?: AnnotationDocument;
|
|
11
11
|
annotationHash?: string;
|
|
12
|
+
assetHash?: string;
|
|
13
|
+
assetOutputs?: string[];
|
|
14
|
+
rewriteImages?: (token: import("marked").Token) => void;
|
|
12
15
|
}
|
|
13
16
|
export interface ManifestFile {
|
|
14
17
|
sourceHash: string;
|
|
@@ -16,6 +19,8 @@ export interface ManifestFile {
|
|
|
16
19
|
annotationHash: string;
|
|
17
20
|
output: string;
|
|
18
21
|
annotations: string;
|
|
22
|
+
assetHash?: string;
|
|
23
|
+
assets?: string[];
|
|
19
24
|
}
|
|
20
25
|
export interface BuildManifest {
|
|
21
26
|
schemaVersion: 1;
|
|
@@ -37,3 +42,6 @@ export interface ConversionResult {
|
|
|
37
42
|
orphanedAnnotations: string[];
|
|
38
43
|
outputRoot: string;
|
|
39
44
|
}
|
|
45
|
+
export interface ConversionOptions {
|
|
46
|
+
onLog?: (message: string) => void;
|
|
47
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ConversionWatcher {
|
|
2
|
+
close(): void;
|
|
3
|
+
}
|
|
4
|
+
export interface ConversionWatchOptions {
|
|
5
|
+
onLog?: (message: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function watchConversionInput(input: string, output: string, rebuild: () => Promise<void>, options?: ConversionWatchOptions): Promise<ConversionWatcher>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { findWatchDirectories } from "./paths.js";
|
|
4
|
+
export async function watchConversionInput(input, output, rebuild, options = {}) {
|
|
5
|
+
const watchers = new Map();
|
|
6
|
+
let timer;
|
|
7
|
+
let building = false;
|
|
8
|
+
let pending = false;
|
|
9
|
+
const outputRelative = relative(input, output);
|
|
10
|
+
const outputInsideInput = outputRelative !== "" &&
|
|
11
|
+
!outputRelative.startsWith(`..${sep}`) &&
|
|
12
|
+
outputRelative !== ".." &&
|
|
13
|
+
!isAbsolute(outputRelative);
|
|
14
|
+
const refresh = async () => {
|
|
15
|
+
const directories = new Set(await findWatchDirectories(input, outputInsideInput ? resolve(output) : undefined));
|
|
16
|
+
for (const [directory, active] of watchers) {
|
|
17
|
+
if (!directories.has(directory)) {
|
|
18
|
+
active.close();
|
|
19
|
+
watchers.delete(directory);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
for (const directory of directories) {
|
|
23
|
+
if (watchers.has(directory))
|
|
24
|
+
continue;
|
|
25
|
+
watchers.set(directory, watch(directory, (eventType, filename) => {
|
|
26
|
+
const target = filename ? String(filename) : "(unknown)";
|
|
27
|
+
options.onLog?.(`Watch event ${eventType}: ${relative(input, resolve(directory, target)) || "."}`);
|
|
28
|
+
schedule();
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const run = async () => {
|
|
33
|
+
if (building) {
|
|
34
|
+
pending = true;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
building = true;
|
|
38
|
+
try {
|
|
39
|
+
options.onLog?.("Watch rebuild started");
|
|
40
|
+
await rebuild();
|
|
41
|
+
await refresh();
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error(`Watch rebuild failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
building = false;
|
|
48
|
+
if (pending) {
|
|
49
|
+
pending = false;
|
|
50
|
+
schedule();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const schedule = () => {
|
|
55
|
+
if (timer)
|
|
56
|
+
clearTimeout(timer);
|
|
57
|
+
timer = setTimeout(run, 100);
|
|
58
|
+
};
|
|
59
|
+
await refresh();
|
|
60
|
+
return {
|
|
61
|
+
close() {
|
|
62
|
+
if (timer)
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
for (const active of watchers.values())
|
|
65
|
+
active.close();
|
|
66
|
+
watchers.clear();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -2,6 +2,6 @@ import type { FileBreadcrumb, FileTreeOptions } from "../../types.js";
|
|
|
2
2
|
export declare function renderFileTree(options?: FileTreeOptions): string;
|
|
3
3
|
export declare function renderFileSidebar(options?: FileTreeOptions): string;
|
|
4
4
|
export declare function renderFileTreeScript(enabled: boolean): string;
|
|
5
|
-
export declare function renderBreadcrumbs(breadcrumbs?: FileBreadcrumb[]
|
|
5
|
+
export declare function renderBreadcrumbs(breadcrumbs?: FileBreadcrumb[]): string;
|
|
6
6
|
export declare function renderModifiedAt(modifiedAt?: string): string;
|
|
7
7
|
export declare function renderModifiedAtScript(enabled: boolean): string;
|