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.
- package/README.md +153 -0
- package/dist/annotations/model.d.ts +29 -0
- package/dist/annotations/model.js +56 -0
- package/dist/annotations/storage.d.ts +4 -0
- package/dist/annotations/storage.js +31 -0
- package/dist/cli/directory.d.ts +4 -0
- package/dist/cli/directory.js +3 -0
- package/dist/cli/open-browser.d.ts +12 -0
- package/dist/cli/open-browser.js +37 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +125 -0
- package/dist/conversion/directory.d.ts +3 -0
- package/dist/conversion/directory.js +208 -0
- package/dist/conversion/gitignore.d.ts +8 -0
- package/dist/conversion/gitignore.js +43 -0
- package/dist/conversion/manifest.d.ts +8 -0
- package/dist/conversion/manifest.js +40 -0
- package/dist/conversion/navigation.d.ts +4 -0
- package/dist/conversion/navigation.js +70 -0
- package/dist/conversion/paths.d.ts +10 -0
- package/dist/conversion/paths.js +86 -0
- package/dist/conversion/rendering.d.ts +5 -0
- package/dist/conversion/rendering.js +39 -0
- package/dist/conversion/single-file.d.ts +1 -0
- package/dist/conversion/single-file.js +35 -0
- package/dist/conversion/types.d.ts +39 -0
- package/dist/conversion/types.js +1 -0
- package/dist/features/annotations/index.d.ts +7 -0
- package/dist/features/annotations/index.js +79 -0
- package/dist/features/annotations.d.ts +9 -0
- package/dist/features/annotations.js +78 -0
- package/dist/features/code-blocks/index.d.ts +5 -0
- package/dist/features/code-blocks/index.js +97 -0
- package/dist/features/code-blocks.d.ts +5 -0
- package/dist/features/code-blocks.js +89 -0
- package/dist/features/file-tree/index.d.ts +7 -0
- package/dist/features/file-tree/index.js +347 -0
- package/dist/features/file-tree.d.ts +7 -0
- package/dist/features/file-tree.js +325 -0
- package/dist/features/header/index.d.ts +4 -0
- package/dist/features/header/index.js +47 -0
- package/dist/features/header.d.ts +6 -0
- package/dist/features/header.js +47 -0
- package/dist/features/sidebar/index.d.ts +10 -0
- package/dist/features/sidebar/index.js +55 -0
- package/dist/features/sidebar.d.ts +13 -0
- package/dist/features/sidebar.js +55 -0
- package/dist/features/table-of-contents/index.d.ts +16 -0
- package/dist/features/table-of-contents/index.js +91 -0
- package/dist/features/table-of-contents.d.ts +16 -0
- package/dist/features/table-of-contents.js +91 -0
- package/dist/features/types.d.ts +6 -0
- package/dist/features/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/markdown-to-html.d.ts +5 -0
- package/dist/markdown-to-html.js +77 -0
- package/dist/server/annotation-repository.d.ts +35 -0
- package/dist/server/annotation-repository.js +184 -0
- package/dist/server/api.d.ts +4 -0
- package/dist/server/api.js +77 -0
- package/dist/server/constants.d.ts +2 -0
- package/dist/server/constants.js +2 -0
- package/dist/server/html-security.d.ts +4 -0
- package/dist/server/html-security.js +61 -0
- package/dist/server/response.d.ts +2 -0
- package/dist/server/response.js +9 -0
- package/dist/server/server.d.ts +3 -0
- package/dist/server/server.js +77 -0
- package/dist/server/static-files.d.ts +2 -0
- package/dist/server/static-files.js +72 -0
- package/dist/server/types.d.ts +16 -0
- package/dist/server/types.js +1 -0
- package/dist/template/document.d.ts +20 -0
- package/dist/template/document.js +33 -0
- package/dist/template/styles.d.ts +5 -0
- package/dist/template/styles.js +109 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.js +1 -0
- package/dist/utils/files.d.ts +1 -0
- package/dist/utils/files.js +9 -0
- package/dist/utils/html.d.ts +2 -0
- package/dist/utils/html.js +17 -0
- package/dist/utils/icons.d.ts +8 -0
- package/dist/utils/icons.js +24 -0
- package/package.json +43 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, } from "node:path";
|
|
3
|
+
import { countActiveAnnotations } from "../annotations/model.js";
|
|
4
|
+
import { readAnnotations, serializeAnnotations, } from "../annotations/storage.js";
|
|
5
|
+
import { renderMarkdown } from "../markdown-to-html.js";
|
|
6
|
+
import { atomicWriteFile } from "../utils/files.js";
|
|
7
|
+
import { BUILD_MANIFEST, assertNoOutputCollisions, loadManifest, writeManifest, } from "./manifest.js";
|
|
8
|
+
import { buildBreadcrumbs, buildFileTree } from "./navigation.js";
|
|
9
|
+
import { DEFAULT_OUTPUT_DIRECTORY, findMarkdownFiles, firstExistingPath, pathExists, toLegacyMetadataPaths, } from "./paths.js";
|
|
10
|
+
import { OUTPUT_COMPATIBILITY_VERSION, contentHash, renderFingerprint, rewriteMarkdownLinks, } from "./rendering.js";
|
|
11
|
+
async function migrateLegacyMetadata(files, output) {
|
|
12
|
+
let moved = 0;
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const metadata = join(output, ...file.metadataPath.split("/"));
|
|
15
|
+
if (await pathExists(metadata))
|
|
16
|
+
continue;
|
|
17
|
+
const legacy = await firstExistingPath(toLegacyMetadataPaths(file.relativePath).map((path) => join(output, ...path.split("/"))));
|
|
18
|
+
if (!legacy)
|
|
19
|
+
continue;
|
|
20
|
+
await mkdir(dirname(metadata), { recursive: true });
|
|
21
|
+
await rename(legacy, metadata);
|
|
22
|
+
moved++;
|
|
23
|
+
}
|
|
24
|
+
return moved;
|
|
25
|
+
}
|
|
26
|
+
async function loadSources(files) {
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const [source, sourceStat] = await Promise.all([
|
|
29
|
+
readFile(file.sourcePath, "utf8"),
|
|
30
|
+
stat(file.sourcePath),
|
|
31
|
+
]);
|
|
32
|
+
file.source = source;
|
|
33
|
+
file.sourceHash = contentHash(file.source);
|
|
34
|
+
file.modifiedAt = sourceStat.mtime.toISOString();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function moveRenamedDocuments(files, previous, output, removed) {
|
|
38
|
+
let deleted = 0;
|
|
39
|
+
let metadataMoved = 0;
|
|
40
|
+
const added = files.filter((file) => !previous?.files[file.relativePath]);
|
|
41
|
+
for (const file of added) {
|
|
42
|
+
const matches = removed.filter((old) => previous?.files[old]?.sourceHash === file.sourceHash &&
|
|
43
|
+
added.filter((candidate) => candidate.sourceHash === file.sourceHash)
|
|
44
|
+
.length === 1);
|
|
45
|
+
if (matches.length !== 1)
|
|
46
|
+
continue;
|
|
47
|
+
const old = matches[0];
|
|
48
|
+
const oldData = previous.files[old];
|
|
49
|
+
const oldMetadata = join(output, ...oldData.annotations.split("/"));
|
|
50
|
+
const nextMetadata = join(output, ...file.metadataPath.split("/"));
|
|
51
|
+
if ((await pathExists(oldMetadata)) && oldMetadata !== nextMetadata) {
|
|
52
|
+
await mkdir(dirname(nextMetadata), { recursive: true });
|
|
53
|
+
const value = await readAnnotations(oldMetadata);
|
|
54
|
+
value.document = file.relativePath;
|
|
55
|
+
await atomicWriteFile(nextMetadata, serializeAnnotations(value));
|
|
56
|
+
await rm(oldMetadata);
|
|
57
|
+
metadataMoved++;
|
|
58
|
+
}
|
|
59
|
+
const oldHtml = join(output, ...oldData.output.split("/"));
|
|
60
|
+
if (await pathExists(oldHtml)) {
|
|
61
|
+
await rm(oldHtml);
|
|
62
|
+
deleted++;
|
|
63
|
+
}
|
|
64
|
+
warnAboutStaleLinks(files, old);
|
|
65
|
+
removed.splice(removed.indexOf(old), 1);
|
|
66
|
+
}
|
|
67
|
+
return { deleted, metadataMoved };
|
|
68
|
+
}
|
|
69
|
+
function warnAboutStaleLinks(files, oldPath) {
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const formerHref = posix.relative(posix.dirname(file.relativePath), oldPath);
|
|
72
|
+
if (file.source?.includes(formerHref)) {
|
|
73
|
+
console.warn(`Possible stale Markdown link in ${file.relativePath}: ${formerHref}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function loadMetadata(files, output) {
|
|
78
|
+
let created = 0;
|
|
79
|
+
for (const file of files) {
|
|
80
|
+
const metadata = join(output, ...file.metadataPath.split("/"));
|
|
81
|
+
const existed = await pathExists(metadata);
|
|
82
|
+
file.annotations = await readAnnotations(metadata, file.relativePath);
|
|
83
|
+
if (!existed)
|
|
84
|
+
created++;
|
|
85
|
+
file.annotationHash = contentHash(serializeAnnotations(file.annotations));
|
|
86
|
+
}
|
|
87
|
+
return created;
|
|
88
|
+
}
|
|
89
|
+
async function removeDeletedHtml(removed, previous, output) {
|
|
90
|
+
let deleted = 0;
|
|
91
|
+
const orphaned = [];
|
|
92
|
+
for (const old of removed) {
|
|
93
|
+
const info = previous?.files[old];
|
|
94
|
+
if (!info)
|
|
95
|
+
continue;
|
|
96
|
+
const oldHtml = join(output, ...info.output.split("/"));
|
|
97
|
+
if (await pathExists(oldHtml)) {
|
|
98
|
+
await rm(oldHtml);
|
|
99
|
+
deleted++;
|
|
100
|
+
}
|
|
101
|
+
if (await pathExists(join(output, ...info.annotations.split("/")))) {
|
|
102
|
+
orphaned.push(info.annotations);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { deleted, orphaned };
|
|
106
|
+
}
|
|
107
|
+
async function renderChangedFiles(files, input, output, previous, full) {
|
|
108
|
+
let converted = 0;
|
|
109
|
+
let skipped = 0;
|
|
110
|
+
const manifestFiles = {};
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
const old = previous?.files[file.relativePath];
|
|
113
|
+
const destination = join(output, ...file.outputPath.split("/"));
|
|
114
|
+
const changed = full ||
|
|
115
|
+
!old ||
|
|
116
|
+
old.sourceHash !== file.sourceHash ||
|
|
117
|
+
old.modifiedAt !== file.modifiedAt ||
|
|
118
|
+
old.annotationHash !== file.annotationHash ||
|
|
119
|
+
!(await pathExists(destination));
|
|
120
|
+
if (changed) {
|
|
121
|
+
await atomicWriteFile(destination, renderMarkdown(file.source, {
|
|
122
|
+
title: basename(file.relativePath, extname(file.relativePath)),
|
|
123
|
+
modifiedAt: file.modifiedAt,
|
|
124
|
+
fileTree: {
|
|
125
|
+
title: "ファイル",
|
|
126
|
+
items: buildFileTree(files, file.outputPath),
|
|
127
|
+
breadcrumbs: buildBreadcrumbs(files, file),
|
|
128
|
+
},
|
|
129
|
+
markedOptions: { walkTokens: rewriteMarkdownLinks },
|
|
130
|
+
}, file.annotations));
|
|
131
|
+
converted++;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
skipped++;
|
|
135
|
+
}
|
|
136
|
+
manifestFiles[file.relativePath] = {
|
|
137
|
+
sourceHash: file.sourceHash,
|
|
138
|
+
modifiedAt: file.modifiedAt,
|
|
139
|
+
annotationHash: file.annotationHash,
|
|
140
|
+
output: file.outputPath,
|
|
141
|
+
annotations: file.metadataPath,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return { converted, skipped, files: manifestFiles };
|
|
145
|
+
}
|
|
146
|
+
export async function convertDirectoryDetailed(input, outputArgument) {
|
|
147
|
+
const output = outputArgument
|
|
148
|
+
? resolve(outputArgument)
|
|
149
|
+
: resolve(DEFAULT_OUTPUT_DIRECTORY);
|
|
150
|
+
const outputRelative = relative(input, output);
|
|
151
|
+
const outputInsideInput = outputRelative !== "" &&
|
|
152
|
+
!outputRelative.startsWith(`..${sep}`) &&
|
|
153
|
+
outputRelative !== ".." &&
|
|
154
|
+
!isAbsolute(outputRelative);
|
|
155
|
+
const files = await findMarkdownFiles(input, outputInsideInput ? output : undefined);
|
|
156
|
+
if (files.length === 0)
|
|
157
|
+
throw new Error(`No Markdown files found in: ${input}`);
|
|
158
|
+
assertNoOutputCollisions(files);
|
|
159
|
+
await mkdir(output, { recursive: true });
|
|
160
|
+
const manifestPath = join(output, BUILD_MANIFEST);
|
|
161
|
+
const loaded = await loadManifest(manifestPath);
|
|
162
|
+
if (loaded.warning)
|
|
163
|
+
console.warn(loaded.warning);
|
|
164
|
+
const previous = loaded.manifest;
|
|
165
|
+
await loadSources(files);
|
|
166
|
+
let annotationsMoved = await migrateLegacyMetadata(files, output);
|
|
167
|
+
const currentPaths = new Set(files.map((file) => file.relativePath));
|
|
168
|
+
const removed = Object.keys(previous?.files ?? {}).filter((path) => !currentPaths.has(path));
|
|
169
|
+
const renamed = await moveRenamedDocuments(files, previous, output, removed);
|
|
170
|
+
annotationsMoved += renamed.metadataMoved;
|
|
171
|
+
const annotationsCreated = await loadMetadata(files, output);
|
|
172
|
+
const treeHash = contentHash(files
|
|
173
|
+
.map((file) => `${file.relativePath}\0${file.annotations ? countActiveAnnotations(file.annotations) : 0}`)
|
|
174
|
+
.sort()
|
|
175
|
+
.join("\n"));
|
|
176
|
+
const fingerprint = renderFingerprint();
|
|
177
|
+
const full = !previous ||
|
|
178
|
+
loaded.warning !== undefined ||
|
|
179
|
+
previous.generator.outputCompatibilityVersion !==
|
|
180
|
+
OUTPUT_COMPATIBILITY_VERSION ||
|
|
181
|
+
previous.generator.renderFingerprint !== fingerprint ||
|
|
182
|
+
previous.treeHash !== treeHash;
|
|
183
|
+
const removedResult = await removeDeletedHtml(removed, previous, output);
|
|
184
|
+
const rendered = await renderChangedFiles(files, input, output, previous, full);
|
|
185
|
+
await writeManifest(manifestPath, {
|
|
186
|
+
schemaVersion: 1,
|
|
187
|
+
generator: {
|
|
188
|
+
name: "marksites",
|
|
189
|
+
version: "0.1.0",
|
|
190
|
+
outputCompatibilityVersion: OUTPUT_COMPATIBILITY_VERSION,
|
|
191
|
+
renderFingerprint: fingerprint,
|
|
192
|
+
},
|
|
193
|
+
treeHash,
|
|
194
|
+
files: rendered.files,
|
|
195
|
+
});
|
|
196
|
+
return {
|
|
197
|
+
converted: rendered.converted,
|
|
198
|
+
skipped: rendered.skipped,
|
|
199
|
+
deleted: renamed.deleted + removedResult.deleted,
|
|
200
|
+
annotationsCreated,
|
|
201
|
+
annotationsMoved,
|
|
202
|
+
orphanedAnnotations: removedResult.orphaned,
|
|
203
|
+
outputRoot: output,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
export async function convertDirectory(input, outputArgument) {
|
|
207
|
+
return (await convertDirectoryDetailed(input, outputArgument)).converted;
|
|
208
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import createIgnore from "ignore";
|
|
2
|
+
interface GitignoreRules {
|
|
3
|
+
basePath: string;
|
|
4
|
+
matcher: ReturnType<typeof createIgnore>;
|
|
5
|
+
}
|
|
6
|
+
export declare function loadGitignoreRules(root: string, directory: string, inherited: GitignoreRules[]): Promise<GitignoreRules[]>;
|
|
7
|
+
export declare function isGitignored(relativePath: string, directory: boolean, rules: GitignoreRules[]): boolean;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join, posix, relative } from "node:path";
|
|
3
|
+
import createIgnore from "ignore";
|
|
4
|
+
export async function loadGitignoreRules(root, directory, inherited) {
|
|
5
|
+
let source;
|
|
6
|
+
try {
|
|
7
|
+
source = await readFile(join(directory, ".gitignore"), "utf8");
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error.code === "ENOENT")
|
|
11
|
+
return inherited;
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
const basePath = relative(root, directory).split("\\").join(posix.sep);
|
|
15
|
+
return [
|
|
16
|
+
...inherited,
|
|
17
|
+
{
|
|
18
|
+
basePath: basePath || ".",
|
|
19
|
+
matcher: createIgnore().add(source),
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
export function isGitignored(relativePath, directory, rules) {
|
|
24
|
+
let ignored = false;
|
|
25
|
+
for (const ruleSet of rules) {
|
|
26
|
+
const scoped = scopePath(relativePath, ruleSet.basePath);
|
|
27
|
+
if (!scoped)
|
|
28
|
+
continue;
|
|
29
|
+
const result = ruleSet.matcher.test(directory ? `${scoped}/` : scoped);
|
|
30
|
+
if (result.ignored)
|
|
31
|
+
ignored = true;
|
|
32
|
+
if (result.unignored)
|
|
33
|
+
ignored = false;
|
|
34
|
+
}
|
|
35
|
+
return ignored;
|
|
36
|
+
}
|
|
37
|
+
function scopePath(relativePath, basePath) {
|
|
38
|
+
if (basePath === ".")
|
|
39
|
+
return relativePath;
|
|
40
|
+
if (!relativePath.startsWith(`${basePath}/`))
|
|
41
|
+
return undefined;
|
|
42
|
+
return relativePath.slice(basePath.length + 1);
|
|
43
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BuildManifest, MarkdownFile } from "./types.js";
|
|
2
|
+
export declare const BUILD_MANIFEST = ".marksites-build.json";
|
|
3
|
+
export declare function assertNoOutputCollisions(files: MarkdownFile[]): void;
|
|
4
|
+
export declare function loadManifest(path: string): Promise<{
|
|
5
|
+
manifest?: BuildManifest;
|
|
6
|
+
warning?: string;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function writeManifest(path: string, manifest: BuildManifest): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { atomicWriteFile } from "../utils/files.js";
|
|
3
|
+
export const BUILD_MANIFEST = ".marksites-build.json";
|
|
4
|
+
export function assertNoOutputCollisions(files) {
|
|
5
|
+
const paths = new Map();
|
|
6
|
+
for (const file of files) {
|
|
7
|
+
for (const output of [file.outputPath, file.metadataPath]) {
|
|
8
|
+
const key = output.toLowerCase();
|
|
9
|
+
if (paths.has(key))
|
|
10
|
+
throw new Error(`Multiple Markdown files map to: ${output}`);
|
|
11
|
+
paths.set(key, file.relativePath);
|
|
12
|
+
}
|
|
13
|
+
if (file.outputPath === BUILD_MANIFEST ||
|
|
14
|
+
file.metadataPath === BUILD_MANIFEST) {
|
|
15
|
+
throw new Error(`Output path is reserved: ${BUILD_MANIFEST}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export async function loadManifest(path) {
|
|
20
|
+
try {
|
|
21
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
22
|
+
if (value.schemaVersion !== 1 ||
|
|
23
|
+
!value.generator ||
|
|
24
|
+
typeof value.treeHash !== "string" ||
|
|
25
|
+
typeof value.files !== "object") {
|
|
26
|
+
throw new Error("unsupported schema");
|
|
27
|
+
}
|
|
28
|
+
return { manifest: value };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error.code === "ENOENT")
|
|
32
|
+
return {};
|
|
33
|
+
return {
|
|
34
|
+
warning: `Ignoring invalid build manifest ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function writeManifest(path, manifest) {
|
|
39
|
+
await atomicWriteFile(path, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
40
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FileBreadcrumb, FileTreeNode } from "../types.js";
|
|
2
|
+
import type { MarkdownFile } from "./types.js";
|
|
3
|
+
export declare function buildFileTree(files: MarkdownFile[], currentOutputPath: string): FileTreeNode[];
|
|
4
|
+
export declare function buildBreadcrumbs(files: MarkdownFile[], current: MarkdownFile): FileBreadcrumb[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { basename, posix } from "node:path";
|
|
2
|
+
import { countActiveAnnotations } from "../annotations/model.js";
|
|
3
|
+
function encodeRelativeHref(path) {
|
|
4
|
+
return path.split(posix.sep).map(encodeURIComponent).join("/");
|
|
5
|
+
}
|
|
6
|
+
export function buildFileTree(files, currentOutputPath) {
|
|
7
|
+
const root = { directories: new Map(), files: [] };
|
|
8
|
+
for (const file of files) {
|
|
9
|
+
const parts = file.relativePath.split("/");
|
|
10
|
+
parts.pop();
|
|
11
|
+
let directory = root;
|
|
12
|
+
for (const part of parts) {
|
|
13
|
+
let child = directory.directories.get(part);
|
|
14
|
+
if (!child) {
|
|
15
|
+
child = { directories: new Map(), files: [] };
|
|
16
|
+
directory.directories.set(part, child);
|
|
17
|
+
}
|
|
18
|
+
directory = child;
|
|
19
|
+
}
|
|
20
|
+
directory.files.push(file);
|
|
21
|
+
}
|
|
22
|
+
function render(directory) {
|
|
23
|
+
return [
|
|
24
|
+
...[...directory.directories]
|
|
25
|
+
.sort(([left], [right]) => left.localeCompare(right, "en"))
|
|
26
|
+
.map(([name, child]) => ({
|
|
27
|
+
type: "directory",
|
|
28
|
+
name,
|
|
29
|
+
children: render(child),
|
|
30
|
+
})),
|
|
31
|
+
...[...directory.files]
|
|
32
|
+
.sort((left, right) => basename(left.relativePath).localeCompare(basename(right.relativePath), "en"))
|
|
33
|
+
.map((file) => ({
|
|
34
|
+
type: "file",
|
|
35
|
+
name: basename(file.relativePath),
|
|
36
|
+
href: encodeRelativeHref(posix.relative(posix.dirname(currentOutputPath), file.outputPath) ||
|
|
37
|
+
posix.basename(file.outputPath)),
|
|
38
|
+
current: file.outputPath === currentOutputPath,
|
|
39
|
+
commentCount: file.annotations
|
|
40
|
+
? countActiveAnnotations(file.annotations)
|
|
41
|
+
: 0,
|
|
42
|
+
})),
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
return render(root);
|
|
46
|
+
}
|
|
47
|
+
function hrefBetween(current, target) {
|
|
48
|
+
return encodeRelativeHref(posix.relative(posix.dirname(current), target) || posix.basename(target));
|
|
49
|
+
}
|
|
50
|
+
export function buildBreadcrumbs(files, current) {
|
|
51
|
+
const findIndex = (directory) => files.find((file) => posix.dirname(file.relativePath) === directory &&
|
|
52
|
+
["index.md", "index.markdown"].includes(posix.basename(file.relativePath).toLowerCase()));
|
|
53
|
+
const result = [];
|
|
54
|
+
const parts = current.relativePath.split("/");
|
|
55
|
+
const fileName = parts.pop();
|
|
56
|
+
let directory = "";
|
|
57
|
+
for (const part of parts) {
|
|
58
|
+
directory = directory ? `${directory}/${part}` : part;
|
|
59
|
+
const index = findIndex(directory);
|
|
60
|
+
result.push({
|
|
61
|
+
name: part,
|
|
62
|
+
href: index
|
|
63
|
+
? hrefBetween(current.outputPath, index.outputPath)
|
|
64
|
+
: undefined,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (fileName)
|
|
68
|
+
result.push({ name: fileName, current: true });
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MarkdownFile } from "./types.js";
|
|
2
|
+
export declare const DEFAULT_OUTPUT_DIRECTORY = "marksites";
|
|
3
|
+
export declare function isMarkdown(path: string): boolean;
|
|
4
|
+
export declare function toHtmlPath(path: string): string;
|
|
5
|
+
export declare function toMetadataPath(path: string): string;
|
|
6
|
+
export declare function toLegacyMetadataPaths(path: string): string[];
|
|
7
|
+
export declare function toPosix(path: string): string;
|
|
8
|
+
export declare function pathExists(path: string): Promise<boolean>;
|
|
9
|
+
export declare function firstExistingPath(paths: string[]): Promise<string | undefined>;
|
|
10
|
+
export declare function findMarkdownFiles(root: string, excludedDirectory?: string): Promise<MarkdownFile[]>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { access, readdir } from "node:fs/promises";
|
|
2
|
+
import { join, posix, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { isGitignored, loadGitignoreRules } from "./gitignore.js";
|
|
4
|
+
import { BUILD_MANIFEST } from "./manifest.js";
|
|
5
|
+
const EXCLUDED_DIRECTORY_NAMES = new Set([
|
|
6
|
+
".git",
|
|
7
|
+
"coverage",
|
|
8
|
+
"dist",
|
|
9
|
+
"node_modules",
|
|
10
|
+
]);
|
|
11
|
+
export const DEFAULT_OUTPUT_DIRECTORY = "marksites";
|
|
12
|
+
export function isMarkdown(path) {
|
|
13
|
+
return /\.(md|markdown)$/i.test(path);
|
|
14
|
+
}
|
|
15
|
+
export function toHtmlPath(path) {
|
|
16
|
+
return path.replace(/\.(md|markdown)$/i, ".html");
|
|
17
|
+
}
|
|
18
|
+
export function toMetadataPath(path) {
|
|
19
|
+
const htmlPath = toHtmlPath(path);
|
|
20
|
+
return posix.join(posix.dirname(htmlPath), `.${posix.basename(htmlPath, posix.extname(htmlPath))}.json`);
|
|
21
|
+
}
|
|
22
|
+
export function toLegacyMetadataPaths(path) {
|
|
23
|
+
const htmlPath = toHtmlPath(path);
|
|
24
|
+
const directory = posix.dirname(htmlPath);
|
|
25
|
+
const name = posix.basename(htmlPath, posix.extname(htmlPath));
|
|
26
|
+
return [
|
|
27
|
+
posix.join(directory, `.${name}.marksites.json`),
|
|
28
|
+
posix.join(directory, `${name}.annotations.json`),
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
export function toPosix(path) {
|
|
32
|
+
return path.split(sep).join(posix.sep);
|
|
33
|
+
}
|
|
34
|
+
export async function pathExists(path) {
|
|
35
|
+
try {
|
|
36
|
+
await access(path);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export async function firstExistingPath(paths) {
|
|
44
|
+
for (const path of paths)
|
|
45
|
+
if (await pathExists(path))
|
|
46
|
+
return path;
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
export async function findMarkdownFiles(root, excludedDirectory) {
|
|
50
|
+
const files = [];
|
|
51
|
+
async function visit(directory, inheritedRules) {
|
|
52
|
+
if (directory !== root &&
|
|
53
|
+
(await pathExists(join(directory, BUILD_MANIFEST)))) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const rules = await loadGitignoreRules(root, directory, inheritedRules);
|
|
57
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
58
|
+
entries.sort((left, right) => left.isDirectory() !== right.isDirectory()
|
|
59
|
+
? left.isDirectory()
|
|
60
|
+
? -1
|
|
61
|
+
: 1
|
|
62
|
+
: left.name.localeCompare(right.name, "en"));
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const absolute = join(directory, entry.name);
|
|
65
|
+
const relativePath = toPosix(relative(root, absolute));
|
|
66
|
+
if (isGitignored(relativePath, entry.isDirectory(), rules))
|
|
67
|
+
continue;
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
if (resolve(absolute) !== excludedDirectory &&
|
|
70
|
+
!EXCLUDED_DIRECTORY_NAMES.has(entry.name)) {
|
|
71
|
+
await visit(absolute, rules);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
else if (entry.isFile() && isMarkdown(entry.name)) {
|
|
75
|
+
files.push({
|
|
76
|
+
sourcePath: absolute,
|
|
77
|
+
relativePath,
|
|
78
|
+
outputPath: toHtmlPath(relativePath),
|
|
79
|
+
metadataPath: toMetadataPath(relativePath),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
await visit(root, []);
|
|
85
|
+
return files;
|
|
86
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Token } from "marked";
|
|
2
|
+
export declare const OUTPUT_COMPATIBILITY_VERSION = 5;
|
|
3
|
+
export declare function contentHash(value: string | Buffer): string;
|
|
4
|
+
export declare function rewriteMarkdownLinks(token: Token): void;
|
|
5
|
+
export declare function renderFingerprint(): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { emptyAnnotationDocument } from "../annotations/model.js";
|
|
3
|
+
import { renderMarkdown } from "../markdown-to-html.js";
|
|
4
|
+
export const OUTPUT_COMPATIBILITY_VERSION = 5;
|
|
5
|
+
export function contentHash(value) {
|
|
6
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
7
|
+
}
|
|
8
|
+
function rewriteMarkdownHref(href) {
|
|
9
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(href) ||
|
|
10
|
+
href.startsWith("//") ||
|
|
11
|
+
href.startsWith("/")) {
|
|
12
|
+
return href;
|
|
13
|
+
}
|
|
14
|
+
return href.replace(/\.(md|markdown)(?=([?#]|$))/i, ".html");
|
|
15
|
+
}
|
|
16
|
+
export function rewriteMarkdownLinks(token) {
|
|
17
|
+
if (token.type === "link")
|
|
18
|
+
token.href = rewriteMarkdownHref(token.href);
|
|
19
|
+
}
|
|
20
|
+
export function renderFingerprint() {
|
|
21
|
+
const representativeHtml = renderMarkdown("## Heading\n\n[Document](guide.md)\n\n```js\nconst value = 1;\n```\n", {
|
|
22
|
+
title: "marksites-render-fingerprint",
|
|
23
|
+
modifiedAt: "2026-01-01T00:00:00.000Z",
|
|
24
|
+
fileTree: {
|
|
25
|
+
title: "ファイル",
|
|
26
|
+
items: [
|
|
27
|
+
{
|
|
28
|
+
type: "file",
|
|
29
|
+
name: "index.md",
|
|
30
|
+
href: "index.html",
|
|
31
|
+
current: true,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
breadcrumbs: [{ name: "docs" }, { name: "index.md", current: true }],
|
|
35
|
+
},
|
|
36
|
+
markedOptions: { walkTokens: rewriteMarkdownLinks },
|
|
37
|
+
}, emptyAnnotationDocument("index.md"));
|
|
38
|
+
return contentHash(`${OUTPUT_COMPATIBILITY_VERSION}\n${representativeHtml}`);
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function convertFile(input: string, outputArgument?: string): Promise<string>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, stat } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
|
+
import { readAnnotations } from "../annotations/storage.js";
|
|
4
|
+
import { renderMarkdown } from "../markdown-to-html.js";
|
|
5
|
+
import { atomicWriteFile } from "../utils/files.js";
|
|
6
|
+
import { DEFAULT_OUTPUT_DIRECTORY, firstExistingPath, isMarkdown, pathExists, } from "./paths.js";
|
|
7
|
+
export async function convertFile(input, outputArgument) {
|
|
8
|
+
if (!isMarkdown(input))
|
|
9
|
+
throw new Error(`Input file must use .md or .markdown: ${input}`);
|
|
10
|
+
const output = outputArgument
|
|
11
|
+
? resolve(outputArgument)
|
|
12
|
+
: resolve(DEFAULT_OUTPUT_DIRECTORY, `${basename(input, extname(input))}.html`);
|
|
13
|
+
const outputName = basename(output, extname(output));
|
|
14
|
+
const metadataPath = join(dirname(output), `.${outputName}.json`);
|
|
15
|
+
const legacyPaths = [
|
|
16
|
+
join(dirname(output), `.${outputName}.marksites.json`),
|
|
17
|
+
join(dirname(output), `${outputName}.annotations.json`),
|
|
18
|
+
];
|
|
19
|
+
if (!(await pathExists(metadataPath))) {
|
|
20
|
+
const legacy = await firstExistingPath(legacyPaths);
|
|
21
|
+
if (legacy)
|
|
22
|
+
await rename(legacy, metadataPath);
|
|
23
|
+
}
|
|
24
|
+
const annotations = await readAnnotations(metadataPath, basename(input));
|
|
25
|
+
const [markdown, sourceStat] = await Promise.all([
|
|
26
|
+
readFile(input, "utf8"),
|
|
27
|
+
stat(input),
|
|
28
|
+
]);
|
|
29
|
+
await mkdir(dirname(output), { recursive: true });
|
|
30
|
+
await atomicWriteFile(output, renderMarkdown(markdown, {
|
|
31
|
+
title: basename(input, extname(input)),
|
|
32
|
+
modifiedAt: sourceStat.mtime.toISOString(),
|
|
33
|
+
}, annotations));
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AnnotationDocument } from "../annotations/model.js";
|
|
2
|
+
export interface MarkdownFile {
|
|
3
|
+
sourcePath: string;
|
|
4
|
+
relativePath: string;
|
|
5
|
+
outputPath: string;
|
|
6
|
+
metadataPath: string;
|
|
7
|
+
source?: string;
|
|
8
|
+
sourceHash?: string;
|
|
9
|
+
modifiedAt?: string;
|
|
10
|
+
annotations?: AnnotationDocument;
|
|
11
|
+
annotationHash?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ManifestFile {
|
|
14
|
+
sourceHash: string;
|
|
15
|
+
modifiedAt: string;
|
|
16
|
+
annotationHash: string;
|
|
17
|
+
output: string;
|
|
18
|
+
annotations: string;
|
|
19
|
+
}
|
|
20
|
+
export interface BuildManifest {
|
|
21
|
+
schemaVersion: 1;
|
|
22
|
+
generator: {
|
|
23
|
+
name: "marksites";
|
|
24
|
+
version: string;
|
|
25
|
+
outputCompatibilityVersion: number;
|
|
26
|
+
renderFingerprint: string;
|
|
27
|
+
};
|
|
28
|
+
treeHash: string;
|
|
29
|
+
files: Record<string, ManifestFile>;
|
|
30
|
+
}
|
|
31
|
+
export interface ConversionResult {
|
|
32
|
+
converted: number;
|
|
33
|
+
skipped: number;
|
|
34
|
+
deleted: number;
|
|
35
|
+
annotationsCreated: number;
|
|
36
|
+
annotationsMoved: number;
|
|
37
|
+
orphanedAnnotations: string[];
|
|
38
|
+
outputRoot: string;
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type AnnotationDocument } from "../../annotations/model.js";
|
|
2
|
+
import type { DocumentFeature } from "../types.js";
|
|
3
|
+
export interface AnnotationsFeature extends DocumentFeature {
|
|
4
|
+
panel: string;
|
|
5
|
+
count: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function createAnnotationsFeature(data?: AnnotationDocument): AnnotationsFeature;
|