@dogsbay/format-gitbook 0.2.0-beta.130 → 0.2.0-beta.132
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 +23 -0
- package/dist/cli.d.ts +10 -13
- package/dist/cli.js +28 -68
- package/dist/corpus.d.ts +2 -0
- package/dist/corpus.js +2 -0
- package/dist/import.d.ts +27 -0
- package/dist/import.js +62 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/links.d.ts +4 -0
- package/dist/links.js +64 -0
- package/dist/loaders/config.d.ts +2 -2
- package/dist/loaders/config.js +8 -8
- package/dist/loaders/folder.d.ts +19 -0
- package/dist/loaders/folder.js +56 -0
- package/dist/loaders/load.d.ts +23 -7
- package/dist/loaders/load.js +52 -174
- package/dist/loaders/summary.d.ts +7 -3
- package/dist/loaders/summary.js +16 -6
- package/dist/migrate.d.ts +32 -0
- package/dist/migrate.js +19 -0
- package/dist/parse.d.ts +7 -1
- package/dist/parse.js +7 -4
- package/dist/project.d.ts +34 -0
- package/dist/project.js +175 -0
- package/package.json +13 -5
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# @dogsbay/format-gitbook
|
|
2
|
+
|
|
3
|
+
GitBook (Git Sync) import for Dogsbay: the `{% %}` blocks translated to Dogsbay Markdown,
|
|
4
|
+
`SUMMARY.md` as the navigation, `.gitbook.yaml` read as data. Nothing in the repository is run.
|
|
5
|
+
|
|
6
|
+
| Entry | For |
|
|
7
|
+
|---|---|
|
|
8
|
+
| `@dogsbay/format-gitbook/migrate` | The browser: a corpus as a map of files → a Dogsbay project as a map of files. No Node |
|
|
9
|
+
| `@dogsbay/format-gitbook` | The parts, with no Node: `parseGitbook`, `gitbookToDogsbayMd`, `importGitbookCorpus`, `gitbookProject` |
|
|
10
|
+
| `@dogsbay/format-gitbook/cli` | Node: the `dogsbay` CLI's format plugin, and a folder as a corpus |
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { migrate } from "@dogsbay/format-gitbook/migrate";
|
|
14
|
+
const { files, ledger, redirects } = await migrate({ files: corpus, siteName: "My Docs" });
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The CLI's `dogsbay migrate-gitbook` runs the same import and project writer, so both give the
|
|
18
|
+
same project. Find the corpus in a repository with `@dogsbay/detect`. See
|
|
19
|
+
[Migrating from GitBook](https://github.com/dogsbay/dogsbay/blob/main/docs/migrating-gitbook.md).
|
|
20
|
+
|
|
21
|
+
## License
|
|
22
|
+
|
|
23
|
+
MIT
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import type { FormatPlugin
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}[];
|
|
12
|
-
}
|
|
13
|
-
export declare function importGitbook(source: string, opts: Record<string, unknown>): Promise<GitbookImportResult>;
|
|
1
|
+
import type { FormatPlugin } from "@dogsbay/types";
|
|
2
|
+
import { type GitbookImportResult } from "./import.js";
|
|
3
|
+
import { type FolderCorpus } from "./loaders/folder.js";
|
|
4
|
+
export { readFolder, readGitbookFolder, type FolderCorpus } from "./loaders/folder.js";
|
|
5
|
+
export { gitbookProject, type GitbookProject, type GitbookProjectOptions } from "./project.js";
|
|
6
|
+
export type { GitbookImportResult } from "./import.js";
|
|
7
|
+
/** Import a GitBook folder. Prints a one-line summary of the ledger, as the CLI always has. */
|
|
8
|
+
export declare function importGitbook(source: string, opts: Record<string, unknown>): Promise<GitbookImportResult & {
|
|
9
|
+
corpus: FolderCorpus;
|
|
10
|
+
}>;
|
|
14
11
|
export declare const plugin: FormatPlugin;
|
package/dist/cli.js
CHANGED
|
@@ -1,78 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FormatPlugin for GitBook as a source format.
|
|
2
|
+
* FormatPlugin for GitBook as a source format (Node: reads a folder).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* the same fragment model AsciiBinder/Antora use.
|
|
4
|
+
* A folder becomes a {@link Corpus} (`loaders/folder.ts`), and the import itself is
|
|
5
|
+
* `importGitbookCorpus`, the same one the browser entry runs. Fragments
|
|
6
|
+
* (`.gitbook/includes/*`) come back with `fragment: true` so the CLI writes them as
|
|
7
|
+
* `{% include %}` targets, not routable pages.
|
|
9
8
|
*
|
|
10
|
-
* The redirects from `.gitbook.yaml` are surfaced on the result so
|
|
11
|
-
*
|
|
9
|
+
* The redirects from `.gitbook.yaml` are surfaced on the result so `migrate-gitbook` can
|
|
10
|
+
* carry them into the site.
|
|
12
11
|
*/
|
|
13
|
-
import { existsSync
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return slug
|
|
20
|
-
.split(/[-_/]/)
|
|
21
|
-
.filter(Boolean)
|
|
22
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
23
|
-
.join(" ");
|
|
24
|
-
}
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { importGitbookCorpus } from "./import.js";
|
|
14
|
+
import { detectGitbook, readGitbookFolder } from "./loaders/folder.js";
|
|
15
|
+
export { readFolder, readGitbookFolder } from "./loaders/folder.js";
|
|
16
|
+
export { gitbookProject } from "./project.js";
|
|
17
|
+
/** Import a GitBook folder. Prints a one-line summary of the ledger, as the CLI always has. */
|
|
25
18
|
export async function importGitbook(source, opts) {
|
|
26
19
|
if (!existsSync(source)) {
|
|
27
20
|
throw new Error(`Source directory not found: ${source}`);
|
|
28
21
|
}
|
|
29
22
|
const space = typeof opts.space === "string" ? opts.space : undefined;
|
|
30
|
-
const
|
|
31
|
-
const
|
|
23
|
+
const corpus = readGitbookFolder(source, space);
|
|
24
|
+
const result = importGitbookCorpus(corpus, {
|
|
32
25
|
space,
|
|
33
|
-
hrefPrefix,
|
|
26
|
+
hrefPrefix: typeof opts.hrefPrefix === "string" ? opts.hrefPrefix : "",
|
|
34
27
|
});
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
let ledgerTotal = 0;
|
|
38
|
-
for (const ps of pageSources) {
|
|
39
|
-
try {
|
|
40
|
-
const content = readFileSync(ps.filePath, "utf-8");
|
|
41
|
-
const { tree, frontmatter, body, ledger } = parseGitbook(content);
|
|
42
|
-
ledgerTotal += ledger.length;
|
|
43
|
-
const headings = extractHeadings(tree);
|
|
44
|
-
const h1 = headings.find((h) => h.depth === 1);
|
|
45
|
-
// A whitespace-only source `title:` must not win — `.trim()` so a
|
|
46
|
-
// blank falls through to the H1 / slug (review #6).
|
|
47
|
-
const fmTitle = typeof frontmatter.title === "string" && frontmatter.title.trim()
|
|
48
|
-
? frontmatter.title
|
|
49
|
-
: undefined;
|
|
50
|
-
const title = fmTitle || h1?.text || prettify(basename(ps.slug));
|
|
51
|
-
pages.push({
|
|
52
|
-
slug: ps.slug,
|
|
53
|
-
title,
|
|
54
|
-
tree,
|
|
55
|
-
headings,
|
|
56
|
-
frontmatter,
|
|
57
|
-
meta: {},
|
|
58
|
-
...(ps.fragment ? { fragment: true } : {}),
|
|
59
|
-
// Translated Dogsbay-MD body — what migrate-gitbook writes to disk.
|
|
60
|
-
bodyMarkdown: body,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
catch (err) {
|
|
64
|
-
failed += 1;
|
|
65
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
66
|
-
console.warn(`gitbook import: failed to convert ${ps.filePath} (slug "${ps.slug}"), skipping: ${msg}`);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
if (failed > 0) {
|
|
70
|
-
console.warn(`gitbook import: ${failed} of ${pageSources.length} page(s) failed and were skipped`);
|
|
28
|
+
for (const e of result.ledger.filter((e) => e.code === "page-failed")) {
|
|
29
|
+
console.warn(`gitbook import: failed to convert ${e.file}, skipping: ${e.message}`);
|
|
71
30
|
}
|
|
72
|
-
|
|
73
|
-
|
|
31
|
+
const mapped = result.ledger.filter((e) => e.code === "unmapped-construct").length;
|
|
32
|
+
if (mapped > 0) {
|
|
33
|
+
console.warn(`gitbook import: ${mapped} construct(s) mapped with caveats (see the ledger)`);
|
|
74
34
|
}
|
|
75
|
-
return {
|
|
35
|
+
return { ...result, corpus };
|
|
76
36
|
}
|
|
77
37
|
export const plugin = {
|
|
78
38
|
name: "gitbook",
|
|
@@ -87,11 +47,11 @@ export const plugin = {
|
|
|
87
47
|
],
|
|
88
48
|
exportOptions: [],
|
|
89
49
|
async import(source, opts) {
|
|
90
|
-
const { pages, nav, assetRoots } = await importGitbook(source, opts);
|
|
91
|
-
// Slugs are relative to the space's docs root (`.gitbook.yaml` `root:`,
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
const only = assetRoots.length === 1 && assetRoots[0].prefix === "" ? assetRoots[0].dir : undefined;
|
|
95
|
-
return { pages, nav, ...(only ? { contentRoot: only } : {}) };
|
|
50
|
+
const { pages, nav, assetRoots, ledger, corpus } = await importGitbook(source, opts);
|
|
51
|
+
// Slugs are relative to the space's docs root (`.gitbook.yaml` `root:`, or the `--space`
|
|
52
|
+
// folder), so references resolve there: a relative `:::openapi` spec or include looked up
|
|
53
|
+
// from the repo root missed.
|
|
54
|
+
const only = assetRoots.length === 1 && assetRoots[0].prefix === "" ? corpus.absolute(assetRoots[0].dir) : undefined;
|
|
55
|
+
return { pages, nav, ledger, ...(only ? { contentRoot: only } : {}) };
|
|
96
56
|
},
|
|
97
57
|
};
|
package/dist/corpus.d.ts
ADDED
package/dist/corpus.js
ADDED
package/dist/import.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitBook corpus → pages, nav, redirects and a ledger, over a {@link Corpus} (no Node, quiet).
|
|
3
|
+
* The one import: the plugin's `import()` (`convert`, `site build`), `migrate-gitbook` and
|
|
4
|
+
* the browser (`./migrate`) all run it.
|
|
5
|
+
*
|
|
6
|
+
* Fragments (`.gitbook/includes/*`) come out with `fragment: true`, so a writer emits them as
|
|
7
|
+
* `{% include %}` targets, not routable pages (the fragment model AsciiBinder/Antora use).
|
|
8
|
+
*/
|
|
9
|
+
import type { ExportPage, ImportLedgerEntry, NavItem } from "@dogsbay/types";
|
|
10
|
+
import type { Corpus } from "./corpus.js";
|
|
11
|
+
import { type GitbookAssetRoot } from "./loaders/load.js";
|
|
12
|
+
export interface GitbookImportOptions {
|
|
13
|
+
/** Import one space (a subfolder with a SUMMARY.md) of a multi-space repo. */
|
|
14
|
+
space?: string;
|
|
15
|
+
/** URL prefix applied to nav hrefs. */
|
|
16
|
+
hrefPrefix?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface GitbookImportResult {
|
|
19
|
+
pages: ExportPage[];
|
|
20
|
+
nav: NavItem[];
|
|
21
|
+
redirects: Record<string, string>;
|
|
22
|
+
spaces: string[];
|
|
23
|
+
/** Docs roots to mirror non-`.md` assets from (corpus-relative folder + slug prefix). */
|
|
24
|
+
assetRoots: GitbookAssetRoot[];
|
|
25
|
+
ledger: ImportLedgerEntry[];
|
|
26
|
+
}
|
|
27
|
+
export declare function importGitbookCorpus(corpus: Corpus, opts?: GitbookImportOptions): GitbookImportResult;
|
package/dist/import.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { extractHeadings } from "@dogsbay/format-dogsbay-md/browser";
|
|
2
|
+
import { loadGitbookCorpus, prettify } from "./loaders/load.js";
|
|
3
|
+
import { followRenames } from "./links.js";
|
|
4
|
+
import { parseGitbook } from "./parse.js";
|
|
5
|
+
export function importGitbookCorpus(corpus, opts = {}) {
|
|
6
|
+
const { pages: sources, nav, redirects, spaces, assetRoots } = loadGitbookCorpus(corpus, opts);
|
|
7
|
+
const pages = [];
|
|
8
|
+
const ledger = [];
|
|
9
|
+
// A page written under another file name (README.md → index.md, a landing page → index.md):
|
|
10
|
+
// links to it follow the rename.
|
|
11
|
+
const renames = new Map();
|
|
12
|
+
for (const ps of sources) {
|
|
13
|
+
if (ps.fragment)
|
|
14
|
+
continue;
|
|
15
|
+
const name = `${ps.slug.slice(ps.slug.lastIndexOf("/") + 1)}.md`;
|
|
16
|
+
if (name !== ps.path.slice(ps.path.lastIndexOf("/") + 1))
|
|
17
|
+
renames.set(ps.path, name);
|
|
18
|
+
}
|
|
19
|
+
for (const ps of sources) {
|
|
20
|
+
try {
|
|
21
|
+
const content = corpus.text(ps.path);
|
|
22
|
+
if (content === undefined)
|
|
23
|
+
throw new Error("the file could not be read");
|
|
24
|
+
const { tree, frontmatter, body, ledger: notes, bodyLine } = parseGitbook(content, { links: (body) => followRenames(body, ps.path, renames) });
|
|
25
|
+
for (const n of notes) {
|
|
26
|
+
ledger.push({
|
|
27
|
+
file: ps.path,
|
|
28
|
+
...(n.line ? { line: n.line + bodyLine - 1 } : {}),
|
|
29
|
+
severity: "warning",
|
|
30
|
+
code: "unmapped-construct",
|
|
31
|
+
message: `${n.construct}: ${n.note}`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
const headings = extractHeadings(tree);
|
|
35
|
+
const h1 = headings.find((h) => h.depth === 1);
|
|
36
|
+
// A whitespace-only source `title:` must not win — `.trim()` so a blank falls through
|
|
37
|
+
// to the H1 / slug (review #6).
|
|
38
|
+
const fmTitle = typeof frontmatter.title === "string" && frontmatter.title.trim() ? frontmatter.title : undefined;
|
|
39
|
+
const title = fmTitle || h1?.text || prettify(ps.slug.slice(ps.slug.lastIndexOf("/") + 1));
|
|
40
|
+
pages.push({
|
|
41
|
+
slug: ps.slug,
|
|
42
|
+
title,
|
|
43
|
+
tree,
|
|
44
|
+
headings,
|
|
45
|
+
frontmatter,
|
|
46
|
+
meta: {},
|
|
47
|
+
...(ps.fragment ? { fragment: true } : {}),
|
|
48
|
+
// Translated Dogsbay-MD body — what the project writer writes.
|
|
49
|
+
bodyMarkdown: body,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
ledger.push({
|
|
54
|
+
file: ps.path,
|
|
55
|
+
severity: "warning",
|
|
56
|
+
code: "page-failed",
|
|
57
|
+
message: `not converted, skipped: ${err instanceof Error ? err.message : String(err)}`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { pages, nav, redirects, spaces, assetRoots, ledger };
|
|
62
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
* @dogsbay/format-gitbook — GitBook (Git Sync + legacy gitbook-cli)
|
|
3
3
|
* format plugin. Imports GitBook corpora into TreeNode[] by translating
|
|
4
4
|
* the Git Sync markdown dialect to Dogsbay-MD, then parsing with the
|
|
5
|
-
* canonical dogsbay-md parser.
|
|
5
|
+
* canonical dogsbay-md parser. No Node in this entry or in `./migrate`;
|
|
6
|
+
* reading a folder is `./cli`.
|
|
6
7
|
*
|
|
7
8
|
* See plans/format-gitbook.md.
|
|
8
9
|
*/
|
|
9
10
|
export { parseGitbook, type ParseGitbookResult } from "./parse.js";
|
|
10
11
|
export { gitbookToDogsbayMd, type TranslateResult, type TranslateLedgerEntry, } from "./translate.js";
|
|
12
|
+
export { corpusFromMap, type Corpus, type CorpusFiles } from "./corpus.js";
|
|
13
|
+
export { importGitbookCorpus, type GitbookImportOptions, type GitbookImportResult } from "./import.js";
|
|
14
|
+
export { gitbookProject, type GitbookProject, type GitbookProjectOptions } from "./project.js";
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
* @dogsbay/format-gitbook — GitBook (Git Sync + legacy gitbook-cli)
|
|
3
3
|
* format plugin. Imports GitBook corpora into TreeNode[] by translating
|
|
4
4
|
* the Git Sync markdown dialect to Dogsbay-MD, then parsing with the
|
|
5
|
-
* canonical dogsbay-md parser.
|
|
5
|
+
* canonical dogsbay-md parser. No Node in this entry or in `./migrate`;
|
|
6
|
+
* reading a folder is `./cli`.
|
|
6
7
|
*
|
|
7
8
|
* See plans/format-gitbook.md.
|
|
8
9
|
*/
|
|
9
10
|
export { parseGitbook } from "./parse.js";
|
|
10
11
|
export { gitbookToDogsbayMd, } from "./translate.js";
|
|
12
|
+
export { corpusFromMap } from "./corpus.js";
|
|
13
|
+
export { importGitbookCorpus } from "./import.js";
|
|
14
|
+
export { gitbookProject } from "./project.js";
|
package/dist/links.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Corpus path of a page → the file name it is written under, when that differs. */
|
|
2
|
+
export type Renames = Map<string, string>;
|
|
3
|
+
/** Rewrite the page links in `markdown`, a page at corpus path `from`. */
|
|
4
|
+
export declare function followRenames(markdown: string, from: string, renames: Renames): string;
|
package/dist/links.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page links that follow a renamed file. The migration writes `README.md` as `index.md`, and a
|
|
3
|
+
* space's landing page (`structure.readme: intro.md`) as `index.md` too, in the same folder.
|
|
4
|
+
* A link that named the old file (`guide/README.md`, `../intro.md#setup`) would then name
|
|
5
|
+
* nothing, so it is rewritten to the new file name; everything else is left as written.
|
|
6
|
+
*
|
|
7
|
+
* Text work over the translated Dogsbay MD, before it is parsed, so the written page and the
|
|
8
|
+
* tree agree. Fenced code and code spans are left alone: a page that SHOWS a link keeps it.
|
|
9
|
+
*/
|
|
10
|
+
import { dirOf, joinPath, normalizePath } from "@dogsbay/detect";
|
|
11
|
+
const FENCE_RE = /^\s*(`{3,}|~{3,})/; // any indent: fences sit inside list items and directives
|
|
12
|
+
/** `](dest)` / `](<dest>)`, a reference definition `[x]: dest`, and `href="dest"`. */
|
|
13
|
+
const DEST_RE = /(\]\(\s*)(<[^>\n]*>|[^)\s]+)|(^ {0,3}\[[^\]\n]+\]:\s*)(<[^>\n]*>|\S+)|(\bhref=")([^"\n]*)(")/g;
|
|
14
|
+
/** Rewrite the page links in `markdown`, a page at corpus path `from`. */
|
|
15
|
+
export function followRenames(markdown, from, renames) {
|
|
16
|
+
if (renames.size === 0)
|
|
17
|
+
return markdown;
|
|
18
|
+
const base = dirOf(from);
|
|
19
|
+
let fence;
|
|
20
|
+
return markdown
|
|
21
|
+
.split("\n")
|
|
22
|
+
.map((line) => {
|
|
23
|
+
const f = FENCE_RE.exec(line);
|
|
24
|
+
if (fence) {
|
|
25
|
+
if (f && f[1][0] === fence[0] && f[1].length >= fence.length && line.trim() === f[1])
|
|
26
|
+
fence = undefined;
|
|
27
|
+
return line;
|
|
28
|
+
}
|
|
29
|
+
if (f) {
|
|
30
|
+
fence = f[1];
|
|
31
|
+
return line;
|
|
32
|
+
}
|
|
33
|
+
// Code spans stay as written: rewrite only the text between them
|
|
34
|
+
return line
|
|
35
|
+
.split(/(`+[^`]*`+)/)
|
|
36
|
+
.map((part, i) => (i % 2 ? part : part.replace(DEST_RE, (m, ...g) => rewrite(m, g, base, renames))))
|
|
37
|
+
.join("");
|
|
38
|
+
})
|
|
39
|
+
.join("\n");
|
|
40
|
+
}
|
|
41
|
+
function rewrite(match, g, base, renames) {
|
|
42
|
+
const [pre, dest, close] = g[0] !== undefined ? [g[0], g[1], ""] : g[2] !== undefined ? [g[2], g[3], ""] : [g[4], g[5], g[6]];
|
|
43
|
+
const angled = dest.startsWith("<") && dest.endsWith(">");
|
|
44
|
+
const raw = angled ? dest.slice(1, -1) : dest;
|
|
45
|
+
const cut = raw.search(/[#?]/);
|
|
46
|
+
const file = cut < 0 ? raw : raw.slice(0, cut);
|
|
47
|
+
// Only relative paths: not a scheme, a site path, or a bare #fragment
|
|
48
|
+
if (!file || /^[a-z][a-z0-9+.-]*:/i.test(file) || file.startsWith("/"))
|
|
49
|
+
return match;
|
|
50
|
+
let decoded = file;
|
|
51
|
+
try {
|
|
52
|
+
decoded = decodeURIComponent(file);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// keep it as written
|
|
56
|
+
}
|
|
57
|
+
const target = normalizePath(joinPath(base, decoded.replace(/^\.\//, "")));
|
|
58
|
+
const name = target === undefined ? undefined : renames.get(target);
|
|
59
|
+
if (name === undefined)
|
|
60
|
+
return match;
|
|
61
|
+
const slash = file.lastIndexOf("/");
|
|
62
|
+
const next = `${file.slice(0, slash + 1)}${name}${cut < 0 ? "" : raw.slice(cut)}`;
|
|
63
|
+
return `${pre}${angled ? `<${next}>` : next}${close}`;
|
|
64
|
+
}
|
package/dist/loaders/config.d.ts
CHANGED
|
@@ -4,5 +4,5 @@ export interface GitbookConfig {
|
|
|
4
4
|
summary: string;
|
|
5
5
|
redirects: Record<string, string>;
|
|
6
6
|
}
|
|
7
|
-
/** Read `.gitbook.yaml`
|
|
8
|
-
export declare function
|
|
7
|
+
/** Read a `.gitbook.yaml`'s text (undefined: no file); defaults for anything absent or unreadable. */
|
|
8
|
+
export declare function parseGitbookConfig(text: string | undefined): GitbookConfig;
|
package/dist/loaders/config.js
CHANGED
|
@@ -8,24 +8,24 @@
|
|
|
8
8
|
* summary: SUMMARY.md # table of contents, default SUMMARY.md
|
|
9
9
|
* redirects:
|
|
10
10
|
* old/path: new/page.md # old URL → target md file
|
|
11
|
+
*
|
|
12
|
+
* js-yaml, not `yaml`: the playground already ships it, and its `load` reads plain data only
|
|
13
|
+
* (no custom tags), which is all `.gitbook.yaml` holds.
|
|
11
14
|
*/
|
|
12
|
-
import
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import YAML from "yaml";
|
|
15
|
+
import * as yaml from "js-yaml";
|
|
15
16
|
const DEFAULTS = {
|
|
16
17
|
root: "./",
|
|
17
18
|
readme: "README.md",
|
|
18
19
|
summary: "SUMMARY.md",
|
|
19
20
|
redirects: {},
|
|
20
21
|
};
|
|
21
|
-
/** Read `.gitbook.yaml`
|
|
22
|
-
export function
|
|
23
|
-
|
|
24
|
-
if (!existsSync(p))
|
|
22
|
+
/** Read a `.gitbook.yaml`'s text (undefined: no file); defaults for anything absent or unreadable. */
|
|
23
|
+
export function parseGitbookConfig(text) {
|
|
24
|
+
if (text === undefined)
|
|
25
25
|
return { ...DEFAULTS };
|
|
26
26
|
let parsed;
|
|
27
27
|
try {
|
|
28
|
-
parsed =
|
|
28
|
+
parsed = yaml.load(text);
|
|
29
29
|
}
|
|
30
30
|
catch {
|
|
31
31
|
return { ...DEFAULTS };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFolder, type FolderCorpus } from "@dogsbay/corpus/folder";
|
|
2
|
+
import { type GitbookConfig } from "./config.js";
|
|
3
|
+
import { type GitbookLoadOptions, type GitbookLoadResult } from "./load.js";
|
|
4
|
+
export { readFolder, type FolderCorpus };
|
|
5
|
+
/** Read `.gitbook.yaml` from `dir` if present; else return defaults. */
|
|
6
|
+
export declare function readGitbookConfig(dir: string): GitbookConfig;
|
|
7
|
+
/**
|
|
8
|
+
* Does this directory look like a GitBook corpus? The importer's own rule (a marker at the
|
|
9
|
+
* top, or subfolders with a SUMMARY.md), over the folder's top two levels, through the shared
|
|
10
|
+
* path detector (so an mdBook's `src/SUMMARY.md` beside `book.toml` is not claimed).
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectGitbook(sourceDir: string): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* A GitBook folder as a corpus, its docs roots walked first: the spaces and each one's
|
|
15
|
+
* `.gitbook.yaml` `root:` are read from the top two levels, the same rule the import applies.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readGitbookFolder(sourceDir: string, space?: string): FolderCorpus;
|
|
18
|
+
/** `loadGitbookCorpus` over a folder. */
|
|
19
|
+
export declare function loadGitbook(sourceDir: string, opts?: GitbookLoadOptions): GitbookLoadResult;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node only: a GitBook folder as a corpus (`@dogsbay/corpus/folder`, its docs roots walked
|
|
3
|
+
* first), and the CLI's on-disk detector. Everything else in the loader reads the corpus, so
|
|
4
|
+
* the CLI and the browser import run the same code.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { detectGitbook as detectGitbookPaths, gitbookSpaces, joinPath, normalizePath } from "@dogsbay/detect";
|
|
9
|
+
import { readFolder } from "@dogsbay/corpus/folder";
|
|
10
|
+
import { parseGitbookConfig } from "./config.js";
|
|
11
|
+
import { loadGitbookCorpus } from "./load.js";
|
|
12
|
+
export { readFolder };
|
|
13
|
+
/** Read `.gitbook.yaml` from `dir` if present; else return defaults. */
|
|
14
|
+
export function readGitbookConfig(dir) {
|
|
15
|
+
const p = join(dir, ".gitbook.yaml");
|
|
16
|
+
return parseGitbookConfig(existsSync(p) ? readFileSync(p, "utf-8") : undefined);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Does this directory look like a GitBook corpus? The importer's own rule (a marker at the
|
|
20
|
+
* top, or subfolders with a SUMMARY.md), over the folder's top two levels, through the shared
|
|
21
|
+
* path detector (so an mdBook's `src/SUMMARY.md` beside `book.toml` is not claimed).
|
|
22
|
+
*/
|
|
23
|
+
export function detectGitbook(sourceDir) {
|
|
24
|
+
try {
|
|
25
|
+
if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory())
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const { paths } = readFolder(sourceDir, 1);
|
|
32
|
+
return detectGitbookPaths(paths) !== undefined && gitbookSpaces(paths).length > 0;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A GitBook folder as a corpus, its docs roots walked first: the spaces and each one's
|
|
36
|
+
* `.gitbook.yaml` `root:` are read from the top two levels, the same rule the import applies.
|
|
37
|
+
*/
|
|
38
|
+
export function readGitbookFolder(sourceDir, space) {
|
|
39
|
+
const top = readFolder(sourceDir, 1);
|
|
40
|
+
const spaces = space ? [space] : gitbookSpaces(top.paths);
|
|
41
|
+
const roots = [];
|
|
42
|
+
for (const s of spaces.length ? spaces : ["."]) {
|
|
43
|
+
const spaceDir = normalizePath(s);
|
|
44
|
+
if (spaceDir === undefined)
|
|
45
|
+
continue;
|
|
46
|
+
const { root } = parseGitbookConfig(top.text(joinPath(spaceDir, ".gitbook.yaml")));
|
|
47
|
+
const docsDir = root && root !== "./" ? normalizePath(joinPath(spaceDir, root)) : spaceDir;
|
|
48
|
+
if (docsDir)
|
|
49
|
+
roots.push(docsDir);
|
|
50
|
+
}
|
|
51
|
+
return readFolder(sourceDir, Infinity, roots);
|
|
52
|
+
}
|
|
53
|
+
/** `loadGitbookCorpus` over a folder. */
|
|
54
|
+
export function loadGitbook(sourceDir, opts = {}) {
|
|
55
|
+
return loadGitbookCorpus(readGitbookFolder(sourceDir, opts.space), opts);
|
|
56
|
+
}
|
package/dist/loaders/load.d.ts
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitBook corpus loader, over a {@link Corpus} (no Node): find the spaces, read each
|
|
3
|
+
* `.gitbook.yaml`, parse `SUMMARY.md` for nav, list every page (including orphans not in the
|
|
4
|
+
* summary), and surface redirects + include fragments. The CLI runs it over a folder
|
|
5
|
+
* (`folder.ts`), the playground over a map of files.
|
|
6
|
+
*
|
|
7
|
+
* Multi-space corpora (e.g. snyk/user-docs holds six spaces, each a dir with its own
|
|
8
|
+
* SUMMARY.md) are detected by `gitbookSpaces` (@dogsbay/detect, the rule detection uses too):
|
|
9
|
+
* when the corpus root has no marker, every direct subfolder with a SUMMARY.md is a space. The
|
|
10
|
+
* default import concatenates all spaces (each summary's nav becomes a top-level section);
|
|
11
|
+
* `space` selects one.
|
|
12
|
+
*
|
|
13
|
+
* Paths are posix and relative to the corpus root. Pages come out in path order, so the same
|
|
14
|
+
* corpus always gives the same result (a folder's listing order is the file system's).
|
|
15
|
+
*/
|
|
1
16
|
import type { NavItem } from "@dogsbay/types";
|
|
17
|
+
import type { Corpus } from "../corpus.js";
|
|
2
18
|
export interface GitbookPageSource {
|
|
3
|
-
|
|
19
|
+
/** The page's file, relative to the corpus root. */
|
|
20
|
+
path: string;
|
|
4
21
|
slug: string;
|
|
5
22
|
/** True for `.gitbook/includes/*` — mirrored as a fragment, not a page. */
|
|
6
23
|
fragment?: boolean;
|
|
7
24
|
}
|
|
8
25
|
/** A resolved docs root to mirror assets from, and its slug prefix. */
|
|
9
26
|
export interface GitbookAssetRoot {
|
|
10
|
-
/**
|
|
27
|
+
/** Docs folder (space folder + `.gitbook.yaml` `root:`), relative to the corpus root. */
|
|
11
28
|
dir: string;
|
|
12
29
|
/** Slug prefix for pages/assets under this root ("" or "<space>/"). */
|
|
13
30
|
prefix: string;
|
|
@@ -16,17 +33,16 @@ export interface GitbookLoadResult {
|
|
|
16
33
|
pages: GitbookPageSource[];
|
|
17
34
|
nav: NavItem[];
|
|
18
35
|
redirects: Record<string, string>;
|
|
19
|
-
/** Space directories detected (relative to
|
|
36
|
+
/** Space directories detected (relative to the corpus root); ≥2 ⇒ multi-space. */
|
|
20
37
|
spaces: string[];
|
|
21
38
|
/** Docs roots to mirror non-`.md` assets from, with their slug prefix. */
|
|
22
39
|
assetRoots: GitbookAssetRoot[];
|
|
23
40
|
}
|
|
24
41
|
export interface GitbookLoadOptions {
|
|
25
|
-
/** Restrict to a single space directory (relative to
|
|
42
|
+
/** Restrict to a single space directory (relative to the corpus root). */
|
|
26
43
|
space?: string;
|
|
27
44
|
/** URL prefix applied to nav hrefs. */
|
|
28
45
|
hrefPrefix?: string;
|
|
29
46
|
}
|
|
30
|
-
|
|
31
|
-
export declare function
|
|
32
|
-
export declare function loadGitbook(sourceDir: string, opts?: GitbookLoadOptions): GitbookLoadResult;
|
|
47
|
+
export declare function loadGitbookCorpus(corpus: Corpus, opts?: GitbookLoadOptions): GitbookLoadResult;
|
|
48
|
+
export declare function prettify(s: string): string;
|
package/dist/loaders/load.js
CHANGED
|
@@ -1,163 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* parse `SUMMARY.md` for nav, enumerate every page (including orphans not
|
|
4
|
-
* in the summary), and surface redirects + include fragments.
|
|
5
|
-
*
|
|
6
|
-
* Multi-space corpora (e.g. snyk/user-docs holds six spaces, each a dir
|
|
7
|
-
* with its own SUMMARY.md) are detected: when the caller doesn't point at
|
|
8
|
-
* a single space, every dir containing a SUMMARY.md is a space. The
|
|
9
|
-
* default import concatenates all spaces (each summary's nav becomes a
|
|
10
|
-
* top-level section); `--space <dir>` selects one.
|
|
11
|
-
*/
|
|
12
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
13
|
-
import { basename, join, relative, sep } from "node:path";
|
|
14
|
-
import { readGitbookConfig } from "./config.js";
|
|
1
|
+
import { gitbookSpaces, gitbookVisible, joinPath, normalizePath, within } from "@dogsbay/detect";
|
|
2
|
+
import { parseGitbookConfig } from "./config.js";
|
|
15
3
|
import { parseSummary, pathToSlug } from "./summary.js";
|
|
16
|
-
/** Does this directory look like a GitBook corpus? */
|
|
17
|
-
export function detectGitbook(sourceDir) {
|
|
18
|
-
if (!existsSync(sourceDir))
|
|
19
|
-
return false;
|
|
20
|
-
try {
|
|
21
|
-
if (!statSync(sourceDir).isDirectory())
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
// A .gitbook.yaml anywhere near the top, or a SUMMARY.md, is the signal.
|
|
28
|
-
if (existsSync(join(sourceDir, ".gitbook.yaml")))
|
|
29
|
-
return true;
|
|
30
|
-
if (existsSync(join(sourceDir, "SUMMARY.md")))
|
|
31
|
-
return true;
|
|
32
|
-
// Or a single-level space dir carrying either marker.
|
|
33
|
-
return findSpaces(sourceDir).length > 0;
|
|
34
|
-
}
|
|
35
4
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* return just `["."]` — we do NOT also scan subdirectories, because a
|
|
40
|
-
* nested `SUMMARY.md` (e.g. a stray one under `api/`) would otherwise be
|
|
41
|
-
* misread as a second space, duplicating that subtree's pages and adding
|
|
42
|
-
* a phantom top-level nav section (review #8). Multi-space is the
|
|
43
|
-
* monorepo shape where the root has NO SUMMARY.md but ≥1 subdir does.
|
|
5
|
+
* The pages under one docs folder: every visible `.md` but SUMMARY and GLOSSARY, and only what
|
|
6
|
+
* stays in that folder once links resolve (a link from the docs root elsewhere in the repo
|
|
7
|
+
* does not bring pages in).
|
|
44
8
|
*/
|
|
45
|
-
function
|
|
46
|
-
if (existsSync(join(sourceDir, "SUMMARY.md")) || existsSync(join(sourceDir, ".gitbook.yaml"))) {
|
|
47
|
-
return ["."];
|
|
48
|
-
}
|
|
9
|
+
function pagesUnder(corpus, paths, docsDir, landing) {
|
|
49
10
|
const out = [];
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
if (existsSync(join(dir, "SUMMARY.md"))) {
|
|
53
|
-
const rel = relative(sourceDir, dir) || ".";
|
|
54
|
-
if (!seen.has(rel)) {
|
|
55
|
-
seen.add(rel);
|
|
56
|
-
out.push(rel);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
// One level down (the snyk monorepo shape: docs/, platform-administration/…).
|
|
61
|
-
let entries = [];
|
|
62
|
-
try {
|
|
63
|
-
entries = readdirSync(sourceDir);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return out;
|
|
67
|
-
}
|
|
68
|
-
for (const name of entries) {
|
|
69
|
-
if (name.startsWith(".") || name === "node_modules")
|
|
11
|
+
for (const path of paths) {
|
|
12
|
+
if (!within(path, docsDir) || path === docsDir)
|
|
70
13
|
continue;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
/** Walk `spaceDir` for every `.md` file; classify includes as fragments. */
|
|
83
|
-
/** Is `p` (once links resolve) `dir` or inside it? False when either does not exist. */
|
|
84
|
-
function insideDir(p, dir) {
|
|
85
|
-
try {
|
|
86
|
-
const real = realpathSync(p);
|
|
87
|
-
const root = realpathSync(dir);
|
|
88
|
-
return real === root || real.startsWith(root + sep);
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return !existsSync(p);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
function walkPages(spaceDir, sourceDir) {
|
|
95
|
-
const out = [];
|
|
96
|
-
// Inside the docs root once links resolve, and each directory once: a
|
|
97
|
-
// symlink out of the repo read pages from elsewhere, and two links to `.`
|
|
98
|
-
// made the walk branch twice per level until ELOOP (about 2^40 visits).
|
|
99
|
-
let realRoot;
|
|
100
|
-
try {
|
|
101
|
-
realRoot = realpathSync(spaceDir);
|
|
102
|
-
}
|
|
103
|
-
catch {
|
|
104
|
-
return out;
|
|
14
|
+
if (corpus.contains && !corpus.contains(path, docsDir))
|
|
15
|
+
continue;
|
|
16
|
+
const rel = docsDir ? path.slice(docsDir.length + 1) : path;
|
|
17
|
+
if (!gitbookVisible(rel))
|
|
18
|
+
continue;
|
|
19
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
20
|
+
if (!name.toLowerCase().endsWith(".md"))
|
|
21
|
+
continue;
|
|
22
|
+
if (name === "SUMMARY.md" || name === "GLOSSARY.md")
|
|
23
|
+
continue;
|
|
24
|
+
out.push({ path, slug: pathToSlug(rel, landing), ...(rel.includes(".gitbook/includes/") ? { fragment: true } : {}) });
|
|
105
25
|
}
|
|
106
|
-
const seen = new Set([realRoot]);
|
|
107
|
-
const walk = (dir) => {
|
|
108
|
-
let entries;
|
|
109
|
-
try {
|
|
110
|
-
entries = readdirSync(dir);
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
for (const name of entries) {
|
|
116
|
-
if (name === "node_modules")
|
|
117
|
-
continue;
|
|
118
|
-
const full = join(dir, name);
|
|
119
|
-
let st;
|
|
120
|
-
let real;
|
|
121
|
-
try {
|
|
122
|
-
real = realpathSync(full);
|
|
123
|
-
if (real !== realRoot && !real.startsWith(realRoot + sep))
|
|
124
|
-
continue;
|
|
125
|
-
st = statSync(real);
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (st.isDirectory()) {
|
|
131
|
-
// Hidden dirs are skipped EXCEPT `.gitbook` (holds includes).
|
|
132
|
-
if (name.startsWith(".") && name !== ".gitbook")
|
|
133
|
-
continue;
|
|
134
|
-
if (seen.has(real))
|
|
135
|
-
continue;
|
|
136
|
-
seen.add(real);
|
|
137
|
-
walk(full);
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
if (!name.toLowerCase().endsWith(".md"))
|
|
141
|
-
continue;
|
|
142
|
-
if (name === "SUMMARY.md" || name === "GLOSSARY.md")
|
|
143
|
-
continue;
|
|
144
|
-
const relFromSpace = relative(spaceDir, full).replace(/\\/g, "/");
|
|
145
|
-
const isInclude = relFromSpace.includes(".gitbook/includes/");
|
|
146
|
-
out.push({
|
|
147
|
-
filePath: full,
|
|
148
|
-
slug: pathToSlug(relFromSpace),
|
|
149
|
-
...(isInclude ? { fragment: true } : {}),
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
};
|
|
153
|
-
walk(spaceDir);
|
|
154
26
|
return out;
|
|
155
27
|
}
|
|
156
|
-
export function
|
|
28
|
+
export function loadGitbookCorpus(corpus, opts = {}) {
|
|
157
29
|
const hrefPrefix = opts.hrefPrefix ?? "";
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
// root itself if none matched).
|
|
30
|
+
const paths = [...corpus.paths].sort();
|
|
31
|
+
const allSpaces = gitbookSpaces(paths);
|
|
32
|
+
// Which spaces to load: an explicit `space`, else all detected (or the root itself if none matched).
|
|
161
33
|
let spaces = opts.space ? [opts.space] : allSpaces;
|
|
162
34
|
if (spaces.length === 0)
|
|
163
35
|
spaces = ["."];
|
|
@@ -167,33 +39,38 @@ export function loadGitbook(sourceDir, opts = {}) {
|
|
|
167
39
|
const assetRoots = [];
|
|
168
40
|
const multi = spaces.length > 1;
|
|
169
41
|
for (const space of spaces) {
|
|
170
|
-
const spaceDir =
|
|
171
|
-
if (
|
|
42
|
+
const spaceDir = normalizePath(space);
|
|
43
|
+
if (spaceDir === undefined)
|
|
44
|
+
throw new Error(`space "${space}" is outside the source directory`);
|
|
45
|
+
if (spaceDir && !paths.some((p) => within(p, spaceDir)))
|
|
172
46
|
continue;
|
|
173
|
-
const config =
|
|
174
|
-
// `root:` relocates the docs dir within the space.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
47
|
+
const config = parseGitbookConfig(corpus.text(joinPath(spaceDir, ".gitbook.yaml")));
|
|
48
|
+
// `root:` relocates the docs dir within the space. It comes from the repo being imported:
|
|
49
|
+
// it may not lead out of it.
|
|
50
|
+
const docsDir = config.root && config.root !== "./" ? normalizePath(joinPath(spaceDir, config.root)) : spaceDir;
|
|
51
|
+
if (docsDir === undefined) {
|
|
178
52
|
throw new Error(`.gitbook.yaml in ${space === "." ? "the source" : space}: root "${config.root}" is outside the source directory`);
|
|
179
53
|
}
|
|
54
|
+
// `structure.readme` names the home page when it is not README.md (`readme: intro.md`).
|
|
55
|
+
// It becomes `index`, like a README; one that is not in the docs root is ignored.
|
|
56
|
+
const readme = normalizePath(config.readme);
|
|
57
|
+
const landing = readme && !/^readme\.md$/i.test(readme) && paths.includes(joinPath(docsDir, readme)) ? readme : undefined;
|
|
180
58
|
// Page prefix so multi-space slugs don't collide (space dir name).
|
|
181
|
-
const slugPrefix = multi &&
|
|
59
|
+
const slugPrefix = multi && spaceDir ? `${spaceDir}/` : "";
|
|
182
60
|
assetRoots.push({ dir: docsDir, prefix: slugPrefix });
|
|
183
|
-
for (const p of
|
|
61
|
+
for (const p of pagesUnder(corpus, paths, docsDir, landing)) {
|
|
184
62
|
pages.push({ ...p, slug: `${slugPrefix}${p.slug}`.replace(/\/+/g, "/") });
|
|
185
63
|
}
|
|
186
64
|
// Nav from SUMMARY.md (under docsDir per config.summary).
|
|
187
|
-
const summaryPath =
|
|
65
|
+
const summaryPath = normalizePath(joinPath(docsDir, config.summary));
|
|
66
|
+
const summary = summaryPath === undefined ? undefined : corpus.text(summaryPath);
|
|
188
67
|
const navPrefix = `${hrefPrefix}${slugPrefix ? "/" + slugPrefix.replace(/\/$/, "") : ""}`;
|
|
189
|
-
if (
|
|
190
|
-
const parsed = parseSummary(
|
|
191
|
-
if (multi)
|
|
68
|
+
if (summary !== undefined) {
|
|
69
|
+
const parsed = parseSummary(summary, navPrefix, landing);
|
|
70
|
+
if (multi)
|
|
192
71
|
nav.push({ label: prettify(space), children: parsed.nav });
|
|
193
|
-
|
|
194
|
-
else {
|
|
72
|
+
else
|
|
195
73
|
nav.push(...parsed.nav);
|
|
196
|
-
}
|
|
197
74
|
}
|
|
198
75
|
else {
|
|
199
76
|
// No SUMMARY.md — infer nav from folder structure (paths → slugs).
|
|
@@ -203,13 +80,14 @@ export function loadGitbook(sourceDir, opts = {}) {
|
|
|
203
80
|
else
|
|
204
81
|
nav.push(...inferred);
|
|
205
82
|
}
|
|
206
|
-
// Redirect keys (old URL) AND values (target .md) are relative to the
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
// that doesn't exist under `docs/`.
|
|
83
|
+
// Redirect keys (old URL) AND values (target .md) are relative to the space, so BOTH get
|
|
84
|
+
// the space slug prefix in a multi-space import — else a redirect in space `docs` would
|
|
85
|
+
// point at an unprefixed target that doesn't exist under `docs/`.
|
|
210
86
|
for (const [k, v] of Object.entries(config.redirects)) {
|
|
211
87
|
const key = slugPrefix ? `${slugPrefix}${k}` : k;
|
|
212
|
-
|
|
88
|
+
// A redirect to the landing page goes to the space's home, as one to its README does
|
|
89
|
+
const target = landing && normalizePath(v) === landing ? "README.md" : v;
|
|
90
|
+
const val = slugPrefix ? `${slugPrefix}${target.replace(/^\.\//, "")}` : target;
|
|
213
91
|
redirects[key] = val;
|
|
214
92
|
}
|
|
215
93
|
}
|
|
@@ -219,10 +97,10 @@ export function loadGitbook(sourceDir, opts = {}) {
|
|
|
219
97
|
function inferNavFromPages(pages) {
|
|
220
98
|
return pages
|
|
221
99
|
.filter((p) => p.slug !== "index")
|
|
222
|
-
.map((p) => ({ label: prettify(
|
|
100
|
+
.map((p) => ({ label: prettify(p.slug.slice(p.slug.lastIndexOf("/") + 1)), href: `/${p.slug}` }))
|
|
223
101
|
.sort((a, b) => a.label.localeCompare(b.label));
|
|
224
102
|
}
|
|
225
|
-
function prettify(s) {
|
|
103
|
+
export function prettify(s) {
|
|
226
104
|
return s
|
|
227
105
|
.split(/[-_/]/)
|
|
228
106
|
.filter(Boolean)
|
|
@@ -22,6 +22,10 @@ export interface ParsedSummary {
|
|
|
22
22
|
/** Every local page path referenced by the summary (posix, .md kept). */
|
|
23
23
|
referenced: string[];
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a GitBook page path to a Dogsbay slug (README → index). `landing` is a space's
|
|
27
|
+
* `.gitbook.yaml` `structure.readme` when it is not README.md: that page is the home page, and
|
|
28
|
+
* a README.md beside it is an ordinary page, `readme`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function pathToSlug(relPath: string, landing?: string): string;
|
|
31
|
+
export declare function parseSummary(content: string, hrefPrefix?: string, landing?: string): ParsedSummary;
|
package/dist/loaders/summary.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a GitBook page path to a Dogsbay slug (README → index). `landing` is a space's
|
|
3
|
+
* `.gitbook.yaml` `structure.readme` when it is not README.md: that page is the home page, and
|
|
4
|
+
* a README.md beside it is an ordinary page, `readme`.
|
|
5
|
+
*/
|
|
6
|
+
export function pathToSlug(relPath, landing) {
|
|
3
7
|
let p = relPath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\.md$/i, "");
|
|
8
|
+
if (landing !== undefined) {
|
|
9
|
+
if (p === landing.replace(/\.md$/i, ""))
|
|
10
|
+
return "index";
|
|
11
|
+
if (p === "README" || p === "readme")
|
|
12
|
+
return "readme";
|
|
13
|
+
}
|
|
4
14
|
if (p === "README" || p === "readme")
|
|
5
15
|
return "index";
|
|
6
16
|
p = p.replace(/\/README$/i, "/index");
|
|
@@ -16,7 +26,7 @@ const LINK_RE = /^\[([^\]]*)\]\(\s*(?:<([^>]*)>|([^)\s]+))[^)]*\)/;
|
|
|
16
26
|
const EXTERNAL_RE = /^(https?:\/\/|mailto:|tel:)/i;
|
|
17
27
|
// Strip a trailing `<a href id>` anchor GitBook adds to group headings.
|
|
18
28
|
const ANCHOR_TAIL_RE = /\s*<a\b[^>]*><\/a>\s*$/;
|
|
19
|
-
export function parseSummary(content, hrefPrefix = "") {
|
|
29
|
+
export function parseSummary(content, hrefPrefix = "", landing) {
|
|
20
30
|
const referenced = [];
|
|
21
31
|
const top = [];
|
|
22
32
|
// Current group container the following items attach to (null = top level).
|
|
@@ -43,7 +53,7 @@ export function parseSummary(content, hrefPrefix = "") {
|
|
|
43
53
|
if (!it)
|
|
44
54
|
continue;
|
|
45
55
|
const indent = it[1].replace(/\t/g, " ").length;
|
|
46
|
-
const item = parseItem(it[2], hrefPrefix, referenced);
|
|
56
|
+
const item = parseItem(it[2], hrefPrefix, referenced, landing);
|
|
47
57
|
if (!item)
|
|
48
58
|
continue;
|
|
49
59
|
// Attach by indentation. Pop deeper-or-equal entries; the remaining
|
|
@@ -62,7 +72,7 @@ export function parseSummary(content, hrefPrefix = "") {
|
|
|
62
72
|
strip(top);
|
|
63
73
|
return { nav: top, referenced };
|
|
64
74
|
}
|
|
65
|
-
function parseItem(text, hrefPrefix, referenced) {
|
|
75
|
+
function parseItem(text, hrefPrefix, referenced, landing) {
|
|
66
76
|
const m = text.match(LINK_RE);
|
|
67
77
|
if (!m) {
|
|
68
78
|
// A plain-text item (no link) — a group-ish header without a page.
|
|
@@ -79,7 +89,7 @@ function parseItem(text, hrefPrefix, referenced) {
|
|
|
79
89
|
const frag = hashIdx === -1 ? "" : target.slice(hashIdx);
|
|
80
90
|
if (filePart)
|
|
81
91
|
referenced.push(filePart.replace(/^\.\//, ""));
|
|
82
|
-
const slug = pathToSlug(filePart);
|
|
92
|
+
const slug = pathToSlug(filePart, landing);
|
|
83
93
|
const href = `${hrefPrefix}/${slug}`.replace(/\/+/g, "/");
|
|
84
94
|
return { label, href: `${href}${frag}` };
|
|
85
95
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@dogsbay/format-gitbook/migrate` — the browser entry. A GitBook corpus as a map of files →
|
|
3
|
+
* a Dogsbay project as a map of files: the same import and project writer `dogsbay
|
|
4
|
+
* migrate-gitbook` runs, without the Astro scaffold. No Node, nothing from the corpus is run,
|
|
5
|
+
* no console, clock or network. Find the corpus with `@dogsbay/detect` first.
|
|
6
|
+
*/
|
|
7
|
+
import type { ImportLedgerEntry } from "@dogsbay/types";
|
|
8
|
+
import { type CorpusFiles } from "./corpus.js";
|
|
9
|
+
export type { CorpusFiles } from "./corpus.js";
|
|
10
|
+
export interface MigrateInput {
|
|
11
|
+
/** The corpus: paths relative to its root (a detection's `root`), text or bytes. */
|
|
12
|
+
files: CorpusFiles;
|
|
13
|
+
/** One space of a multi-space repo (a detection's `choices.spaces`); all when omitted. */
|
|
14
|
+
choose?: {
|
|
15
|
+
space?: string;
|
|
16
|
+
};
|
|
17
|
+
siteName?: string;
|
|
18
|
+
siteUrl?: string;
|
|
19
|
+
/** Where the corpus came from, for MIGRATION.md (e.g. the repo URL). */
|
|
20
|
+
sourceLabel?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface MigrateResult {
|
|
23
|
+
/** The Dogsbay project: dogsbay.config.yml, content/** (pages, nav.yml, assets), MIGRATION.md. */
|
|
24
|
+
files: Map<string, string | Uint8Array>;
|
|
25
|
+
ledger: ImportLedgerEntry[];
|
|
26
|
+
/** Old URL → served URL, both root-absolute; the host decides how to serve them. */
|
|
27
|
+
redirects: {
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
}[];
|
|
31
|
+
}
|
|
32
|
+
export declare function migrate(input: MigrateInput): Promise<MigrateResult>;
|
package/dist/migrate.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { corpusFromMap } from "./corpus.js";
|
|
2
|
+
import { importGitbookCorpus } from "./import.js";
|
|
3
|
+
import { gitbookProject } from "./project.js";
|
|
4
|
+
export async function migrate(input) {
|
|
5
|
+
const corpus = corpusFromMap(input.files);
|
|
6
|
+
const imported = importGitbookCorpus(corpus, { space: input.choose?.space });
|
|
7
|
+
const project = gitbookProject(imported, corpus, {
|
|
8
|
+
siteName: input.siteName?.trim() || "Docs",
|
|
9
|
+
siteUrl: input.siteUrl,
|
|
10
|
+
sourceLabel: input.sourceLabel ?? "GitBook corpus",
|
|
11
|
+
});
|
|
12
|
+
const files = new Map(project.files);
|
|
13
|
+
for (const { from, to } of project.assets) {
|
|
14
|
+
const bytes = input.files.get(from);
|
|
15
|
+
if (bytes !== undefined)
|
|
16
|
+
files.set(to, bytes);
|
|
17
|
+
}
|
|
18
|
+
return { files, ledger: project.ledger, redirects: project.redirects };
|
|
19
|
+
}
|
package/dist/parse.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface ParseGitbookResult {
|
|
|
7
7
|
markdown: string;
|
|
8
8
|
/** Translated body only (no frontmatter) — for callers writing their own. */
|
|
9
9
|
body: string;
|
|
10
|
+
/** Ledger lines count from the body: the source line the body starts on (1 with no front matter). */
|
|
11
|
+
bodyLine: number;
|
|
10
12
|
ledger: TranslateLedgerEntry[];
|
|
11
13
|
}
|
|
12
|
-
export
|
|
14
|
+
export interface ParseGitbookOptions {
|
|
15
|
+
/** Applied to the translated body before it is parsed (the import's renamed page links). */
|
|
16
|
+
links?: (body: string) => string;
|
|
17
|
+
}
|
|
18
|
+
export declare function parseGitbook(src: string, opts?: ParseGitbookOptions): ParseGitbookResult;
|
package/dist/parse.js
CHANGED
|
@@ -15,18 +15,21 @@
|
|
|
15
15
|
* writes to disk — canonical, editable Dogsbay-MD. `tree` feeds the
|
|
16
16
|
* runtime path (format-astro emit, headings, title).
|
|
17
17
|
*/
|
|
18
|
-
import { dogsbayMdToTree } from "@dogsbay/format-dogsbay-md";
|
|
18
|
+
import { dogsbayMdToTree } from "@dogsbay/format-dogsbay-md/browser";
|
|
19
19
|
import { gitbookToDogsbayMd } from "./translate.js";
|
|
20
20
|
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n?/;
|
|
21
|
-
export function parseGitbook(src) {
|
|
21
|
+
export function parseGitbook(src, opts = {}) {
|
|
22
22
|
const normalized = src.replace(/\r\n?/g, "\n");
|
|
23
23
|
const fmMatch = normalized.match(FRONTMATTER_RE);
|
|
24
24
|
const fmBlock = fmMatch ? fmMatch[0] : "";
|
|
25
25
|
const body = fmMatch ? normalized.slice(fmMatch[0].length) : normalized;
|
|
26
|
-
const
|
|
26
|
+
const translated = gitbookToDogsbayMd(body);
|
|
27
|
+
const { ledger } = translated;
|
|
28
|
+
const translatedBody = opts.links ? opts.links(translated.markdown) : translated.markdown;
|
|
27
29
|
const markdown = fmBlock ? `${fmBlock.replace(/\n?$/, "\n")}\n${translatedBody}` : translatedBody;
|
|
28
30
|
// dogsbayMdToTree extracts frontmatter itself (gray-matter) and parses
|
|
29
31
|
// the body into TreeNode[]. Feed it the recombined markdown.
|
|
30
32
|
const { tree, frontmatter } = dogsbayMdToTree(markdown);
|
|
31
|
-
|
|
33
|
+
const bodyLine = fmBlock ? fmBlock.split("\n").length - (fmBlock.endsWith("\n") ? 1 : 0) + 1 : 1;
|
|
34
|
+
return { tree, frontmatter: frontmatter ?? {}, markdown, body: translatedBody, bodyLine, ledger };
|
|
32
35
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ImportLedgerEntry } from "@dogsbay/types";
|
|
2
|
+
import type { Corpus } from "./corpus.js";
|
|
3
|
+
import type { GitbookImportResult } from "./import.js";
|
|
4
|
+
export interface GitbookProjectOptions {
|
|
5
|
+
siteName: string;
|
|
6
|
+
siteUrl?: string;
|
|
7
|
+
/** Where the corpus came from, for MIGRATION.md (a folder, a repo URL). */
|
|
8
|
+
sourceLabel: string;
|
|
9
|
+
/** Where the host writes the redirects, for MIGRATION.md (the CLI: `astro/public/_redirects`). */
|
|
10
|
+
redirectsFile?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface GitbookProject {
|
|
13
|
+
/** The project's text files, by path relative to the project root. */
|
|
14
|
+
files: Map<string, string>;
|
|
15
|
+
/** Files to copy unchanged: corpus path → project path. */
|
|
16
|
+
assets: {
|
|
17
|
+
from: string;
|
|
18
|
+
to: string;
|
|
19
|
+
}[];
|
|
20
|
+
/** Old URL → served URL, both root-absolute (`/old/path` → `/guide`). */
|
|
21
|
+
redirects: {
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
}[];
|
|
25
|
+
/** The import's ledger plus the writer's own notes (slug collisions). */
|
|
26
|
+
ledger: ImportLedgerEntry[];
|
|
27
|
+
stats: {
|
|
28
|
+
pages: number;
|
|
29
|
+
fragments: number;
|
|
30
|
+
collisions: number;
|
|
31
|
+
spaces: string[];
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export declare function gitbookProject(result: GitbookImportResult, corpus: Corpus, opts: GitbookProjectOptions): GitbookProject;
|
package/dist/project.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An imported GitBook corpus → a Dogsbay project, as data (no Node, quiet, deterministic):
|
|
3
|
+
* `content/<slug>.md` (front matter + translated body), fragments as include targets,
|
|
4
|
+
* `content/nav.yml` from SUMMARY.md, `dogsbay.config.yml`, `MIGRATION.md`, the assets to copy
|
|
5
|
+
* and the redirects. `migrate-gitbook` writes it to disk and adds the Astro scaffold and
|
|
6
|
+
* `_redirects`; the browser (`./migrate`) returns it as a map of files.
|
|
7
|
+
*/
|
|
8
|
+
import * as yaml from "js-yaml";
|
|
9
|
+
import { gitbookVisible, within } from "@dogsbay/detect";
|
|
10
|
+
function yamlQuote(s) {
|
|
11
|
+
return /[:#&*!|>%@`,'"[\]{}]|^\s|\s$/.test(s) ? JSON.stringify(s) : s;
|
|
12
|
+
}
|
|
13
|
+
/** Serialize NavItem[] to nav.yml (mirrors migrate-asciidoc's navToYaml). */
|
|
14
|
+
function navToYaml(items, indent = 0) {
|
|
15
|
+
const pad = " ".repeat(indent);
|
|
16
|
+
const lines = [];
|
|
17
|
+
for (const item of items) {
|
|
18
|
+
const hasChildren = item.children !== undefined && item.children.length > 0;
|
|
19
|
+
const target = item.href === undefined
|
|
20
|
+
? undefined
|
|
21
|
+
: /^(?:https?:\/\/|mailto:|tel:)/i.test(item.href) || item.href.includes("#")
|
|
22
|
+
? item.href
|
|
23
|
+
: `${item.href.replace(/^\/+/, "")}.md`;
|
|
24
|
+
if (target !== undefined && !hasChildren) {
|
|
25
|
+
lines.push(`${pad}- ${yamlQuote(item.label)}: ${target}`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
lines.push(`${pad}- ${yamlQuote(item.label)}:`);
|
|
29
|
+
if (target !== undefined)
|
|
30
|
+
lines.push(`${pad} - ${yamlQuote(target)}`);
|
|
31
|
+
if (hasChildren)
|
|
32
|
+
lines.push(navToYaml(item.children, indent + 2));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return lines.join("\n");
|
|
36
|
+
}
|
|
37
|
+
function buildConfig(siteName, siteUrl) {
|
|
38
|
+
const lines = [`schemaVersion: 1`, `site:`, ` name: ${yamlQuote(siteName)}`, ` basePath: ""`];
|
|
39
|
+
if (siteUrl)
|
|
40
|
+
lines.push(` url: ${yamlQuote(siteUrl)}`);
|
|
41
|
+
lines.push(`content:`, ` sources:`, ` - path: ./content`, ` from: dogsbay-md`,
|
|
42
|
+
// `.gitbook/includes` fragments are `{% include %}` targets, not routable pages.
|
|
43
|
+
// routablesFrom: nav restricts routes to SUMMARY-named pages; the fragments stay on disk
|
|
44
|
+
// so includes resolve.
|
|
45
|
+
` excludeFromRoutes:`, ` - .gitbook`, ` routablesFrom: nav`, ``);
|
|
46
|
+
return lines.join("\n");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Front matter (source keys + a title) as a YAML block. Array/object values (`tags: [a, b]`,
|
|
50
|
+
* GitBook `cover:` maps) round-trip instead of being dropped (review #4). The title is the
|
|
51
|
+
* source `title:` only when it's a non-empty string; otherwise the computed title (H1 / slug):
|
|
52
|
+
* a blank source title must NOT clobber it (review #6).
|
|
53
|
+
*/
|
|
54
|
+
// No `noRefs`: an alias the source used stays an alias. Writing each out in full turned ten
|
|
55
|
+
// lines of nested aliases into hundreds of MB (an alias bomb, review 2026-10-20).
|
|
56
|
+
function frontmatterBlock(fm, title) {
|
|
57
|
+
const sourceTitle = typeof fm.title === "string" && fm.title.trim() ? fm.title : undefined;
|
|
58
|
+
const out = { title: sourceTitle ?? title, ...fm };
|
|
59
|
+
out.title = sourceTitle ?? title;
|
|
60
|
+
const body = yaml.dump(out, { lineWidth: -1, skipInvalid: true }).replace(/\n$/, "");
|
|
61
|
+
return `---\n${body}\n---\n\n`;
|
|
62
|
+
}
|
|
63
|
+
/** A redirect's target as the URL it is served at: drop ./ and .md; a README is its folder. */
|
|
64
|
+
function servedUrl(target) {
|
|
65
|
+
// A bare `README` maps to the site root `/`, not a dead `/README` (review #5).
|
|
66
|
+
return `/${target.replace(/^\.\//, "").replace(/\.md$/i, "").replace(/(^|\/)README$/i, "$1").replace(/\/$/, "")}`;
|
|
67
|
+
}
|
|
68
|
+
function migrationNotes(a) {
|
|
69
|
+
const lines = [
|
|
70
|
+
`# Migration: ${a.siteName}`,
|
|
71
|
+
``,
|
|
72
|
+
`Migrated from a GitBook Git Sync corpus by \`dogsbay migrate-gitbook\`.`,
|
|
73
|
+
``,
|
|
74
|
+
`- **Source**: \`${a.sourceLabel}\``,
|
|
75
|
+
`- **Pages**: ${a.pages}${a.fragments ? ` (+ ${a.fragments} include fragments)` : ""}`,
|
|
76
|
+
];
|
|
77
|
+
if (a.spaces.length > 1)
|
|
78
|
+
lines.push(`- **Spaces**: ${a.spaces.join(", ")} (each a top-level nav section)`);
|
|
79
|
+
if (a.redirects)
|
|
80
|
+
lines.push(`- **Redirects**: ${a.redirects}${a.redirectsFile ? ` carried into \`${a.redirectsFile}\`` : ""}`);
|
|
81
|
+
lines.push(``, `## What was converted`, ``, `- GitBook \`{% hint %}\` → \`:::note/:::warning/:::info/:::danger\` callouts`, `- \`{% tabs %}\` / \`{% stepper %}\` / \`{% columns %}\` → \`:::tabs\` / \`:::steps\` / \`:::grid\``, `- \`{% code %}\`, \`{% content-ref %}\`, \`{% embed %}\`, \`{% file %}\`, \`{% page-ref %}\` → native Dogsbay-MD`, `- \`{% include %}\` reusable content preserved (resolved at build time)`, `- \`<figure>\` images, GitBook emoji spans, raw HTML tables handled`, `- \`SUMMARY.md\` → \`content/nav.yml\`; \`.gitbook.yaml\` redirects kept`, ``, `## Known caveats`, ``, `- \`{% openapi %}\` / \`{% swagger %}\` blocks emit \`:::api-endpoint\` placeholders — wire them to the OpenAPI spec for full rendering.`);
|
|
82
|
+
if (a.ledger.length) {
|
|
83
|
+
lines.push(``, `## Review these`, ``);
|
|
84
|
+
for (const e of a.ledger)
|
|
85
|
+
lines.push(`- \`${e.file ?? "?"}${e.line ? `:${e.line}` : ""}\`: ${e.message}`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n") + "\n";
|
|
88
|
+
}
|
|
89
|
+
export function gitbookProject(result, corpus, opts) {
|
|
90
|
+
const files = new Map();
|
|
91
|
+
const ledger = [...result.ledger];
|
|
92
|
+
// Two source files can slugify to the same path (README.md and index.md both → `index`).
|
|
93
|
+
// Keep the first, skip and note the rest rather than silently overwriting a page (review #3).
|
|
94
|
+
// Paths compared without case: on macOS and Windows `INDEX.md` overwrites `index.md`.
|
|
95
|
+
const taken = new Set();
|
|
96
|
+
let fragments = 0;
|
|
97
|
+
let collisions = 0;
|
|
98
|
+
for (const page of result.pages) {
|
|
99
|
+
const dst = `content/${page.slug}.md`;
|
|
100
|
+
if (taken.has(dst.toLowerCase())) {
|
|
101
|
+
collisions += 1;
|
|
102
|
+
ledger.push({ severity: "warning", code: "slug-collision", message: `two pages map to ${dst}: kept the first, skipped a later one` });
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
taken.add(dst.toLowerCase());
|
|
106
|
+
const body = page.bodyMarkdown ?? "";
|
|
107
|
+
const text = body.endsWith("\n") ? body : body + "\n";
|
|
108
|
+
if (page.fragment) {
|
|
109
|
+
// Fragments: pure body, no frontmatter (they're include targets).
|
|
110
|
+
files.set(dst, text);
|
|
111
|
+
fragments += 1;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
files.set(dst, frontmatterBlock(page.frontmatter ?? {}, page.title) + text);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Non-`.md` files under each docs root, at the same relative place under content/, so the
|
|
118
|
+
// translated relative image refs resolve. From the docs root, not the repo root: with
|
|
119
|
+
// `root: ./docs/gitbook/` unrelated repo source must not land in content/.
|
|
120
|
+
const assets = [];
|
|
121
|
+
for (const root of result.assetRoots) {
|
|
122
|
+
for (const path of corpus.paths) {
|
|
123
|
+
if (!within(path, root.dir) || path === root.dir)
|
|
124
|
+
continue;
|
|
125
|
+
// Only what stays in the docs root once links resolve: a link named `env.txt` to the
|
|
126
|
+
// repo's `.env`, or `g` to `.git`, must not be published with the site.
|
|
127
|
+
if (corpus.contains && !corpus.contains(path, root.dir))
|
|
128
|
+
continue;
|
|
129
|
+
const rel = root.dir ? path.slice(root.dir.length + 1) : path;
|
|
130
|
+
if (!gitbookVisible(rel))
|
|
131
|
+
continue;
|
|
132
|
+
const name = rel.slice(rel.lastIndexOf("/") + 1);
|
|
133
|
+
// A hidden file is the repo's (`.env`, `.npmrc`, `.gitbook/assets/.DS_Store`), never a
|
|
134
|
+
// page's asset; `.gitbook.yaml` is read as config, not published.
|
|
135
|
+
if (name.toLowerCase().endsWith(".md") || name.startsWith("."))
|
|
136
|
+
continue;
|
|
137
|
+
assets.push({ from: path, to: `content/${root.prefix}${rel}` });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
files.set("content/nav.yml", `# Migrated by dogsbay migrate-gitbook.\n# Edit freely.\n\n${navToYaml(result.nav)}\n`);
|
|
141
|
+
files.set("dogsbay.config.yml", buildConfig(opts.siteName, opts.siteUrl));
|
|
142
|
+
files.set("MIGRATION.md", "");
|
|
143
|
+
// A corpus file at a generated path (a `nav.yml` in the docs root) must not replace it, nor
|
|
144
|
+
// may two assets that differ only in case replace each other
|
|
145
|
+
for (const f of files.keys())
|
|
146
|
+
taken.add(f.toLowerCase());
|
|
147
|
+
const copies = assets.filter((a) => {
|
|
148
|
+
const key = a.to.toLowerCase();
|
|
149
|
+
if (!taken.has(key)) {
|
|
150
|
+
taken.add(key);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
ledger.push({ file: a.from, severity: "warning", code: "file-replaced", message: `not copied: ${a.to} would replace a file the migration writes` });
|
|
154
|
+
return false;
|
|
155
|
+
});
|
|
156
|
+
// A redirect is one `_redirects` line: no whitespace or control character in either side, or
|
|
157
|
+
// a crafted key (`"x https://evil 301\n/y"`) adds a line of its own. A backslash is a slash
|
|
158
|
+
// to a browser (`/\evil.example` is another host), and a `*` or `:name` in the old path is a
|
|
159
|
+
// pattern that would capture the whole site.
|
|
160
|
+
const redirects = [];
|
|
161
|
+
for (const [from, target] of Object.entries(result.redirects)) {
|
|
162
|
+
const r = { from: `/${from.replace(/^\/+/, "")}`, to: servedUrl(target) };
|
|
163
|
+
if (/[\s\0-\x1f\\]/.test(r.from + r.to) ||
|
|
164
|
+
r.from.startsWith("//") ||
|
|
165
|
+
r.to.startsWith("//") ||
|
|
166
|
+
/\*|(^|\/):/.test(r.from)) {
|
|
167
|
+
ledger.push({ file: ".gitbook.yaml", severity: "warning", code: "redirect-refused", message: `redirect not kept: ${JSON.stringify(from)} → ${JSON.stringify(target)}` });
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
redirects.push(r);
|
|
171
|
+
}
|
|
172
|
+
const pages = result.pages.filter((p) => !p.fragment).length;
|
|
173
|
+
files.set("MIGRATION.md", migrationNotes({ ...opts, pages, fragments, spaces: result.spaces, redirects: redirects.length, ledger }));
|
|
174
|
+
return { files, assets: copies, redirects, ledger, stats: { pages, fragments, collisions, spaces: result.spaces } };
|
|
175
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dogsbay/format-gitbook",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
4
|
-
"description": "GitBook format plugin for Dogsbay — imports GitBook Git Sync (and legacy gitbook-cli) corpora
|
|
3
|
+
"version": "0.2.0-beta.132",
|
|
4
|
+
"description": "GitBook format plugin for Dogsbay — imports GitBook Git Sync (and legacy gitbook-cli) corpora by translating the Git Sync markdown dialect to Dogsbay-MD. ./migrate converts a corpus given as a map of files in the browser (no Node), the same import the CLI runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
"./cli": {
|
|
14
14
|
"types": "./dist/cli.d.ts",
|
|
15
15
|
"import": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"./migrate": {
|
|
18
|
+
"types": "./dist/migrate.d.ts",
|
|
19
|
+
"import": "./dist/migrate.js"
|
|
16
20
|
}
|
|
17
21
|
},
|
|
18
22
|
"files": [
|
|
@@ -20,12 +24,16 @@
|
|
|
20
24
|
"README.md"
|
|
21
25
|
],
|
|
22
26
|
"dependencies": {
|
|
23
|
-
"yaml": "^2.
|
|
24
|
-
"@dogsbay/
|
|
25
|
-
"@dogsbay/
|
|
27
|
+
"js-yaml": "^5.2.1",
|
|
28
|
+
"@dogsbay/corpus": "0.2.0-beta.132",
|
|
29
|
+
"@dogsbay/format-dogsbay-md": "0.2.0-beta.132",
|
|
30
|
+
"@dogsbay/types": "0.2.0-beta.132",
|
|
31
|
+
"@dogsbay/detect": "0.2.0-beta.132"
|
|
26
32
|
},
|
|
27
33
|
"devDependencies": {
|
|
34
|
+
"@types/js-yaml": "^4.0.9",
|
|
28
35
|
"@types/node": "^25.9.5",
|
|
36
|
+
"esbuild": "^0.25.0",
|
|
29
37
|
"typescript": "^5.9.3",
|
|
30
38
|
"vitest": "^4.1.10"
|
|
31
39
|
},
|