@dogsbay/format-gitbook 0.2.0-beta.130 → 0.2.0-beta.131

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 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, ExportPage, NavItem } from "@dogsbay/types";
2
- export interface GitbookImportResult {
3
- pages: ExportPage[];
4
- nav: NavItem[];
5
- redirects: Record<string, string>;
6
- spaces: string[];
7
- /** Docs roots to mirror non-`.md` assets from (dir + slug prefix). */
8
- assetRoots: {
9
- dir: string;
10
- prefix: string;
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
- * Orchestrates: detect corpus → loadGitbook (config + SUMMARY + pages) →
5
- * translate each page (parseGitbook) → assemble ExportPage[] + NavItem[].
6
- * Fragments (`.gitbook/includes/*`) are emitted with `fragment: true` so
7
- * the CLI writes them as `{% include %}` targets, not routable pages —
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
- * `migrate-gitbook` can carry them into the site config.
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, readFileSync } from "node:fs";
14
- import { basename } from "node:path";
15
- import { extractHeadings } from "@dogsbay/format-dogsbay-md";
16
- import { parseGitbook } from "./parse.js";
17
- import { loadGitbook, detectGitbook } from "./loaders/load.js";
18
- function prettify(slug) {
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 hrefPrefix = typeof opts.hrefPrefix === "string" ? opts.hrefPrefix : "";
31
- const { pages: pageSources, nav, redirects, spaces, assetRoots } = loadGitbook(source, {
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 pages = [];
36
- let failed = 0;
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
- if (ledgerTotal > 0) {
73
- console.warn(`gitbook import: ${ledgerTotal} construct(s) mapped with caveats (see per-page ledger)`);
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 { pages, nav, redirects, spaces, assetRoots };
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
- // or the `--space` folder), so references resolve there: a relative
93
- // `:::openapi` spec or include looked up from the repo root missed.
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
  };
@@ -0,0 +1,18 @@
1
+ export interface Corpus {
2
+ /** Every file, posix, relative to the corpus root; plain paths only (`isPlainPath`). */
3
+ paths: string[];
4
+ /** The file's text, or undefined when it is not in the corpus. */
5
+ text(path: string): string | undefined;
6
+ /**
7
+ * Does `path` still lie inside the folder `dir` once links resolve? A folder corpus answers
8
+ * (a symlink in the docs root must not bring in the repo's `.env` or `.git`); a map of files
9
+ * has no links and leaves it out.
10
+ */
11
+ contains?(path: string, dir: string): boolean;
12
+ }
13
+ export type CorpusFiles = Map<string, string | Uint8Array>;
14
+ /**
15
+ * A corpus over a map of files; bytes are read as UTF-8. A key that is not a plain path
16
+ * (`../x`, `/etc/x`, `a/./b`, a backslash) is left out: the keys come from someone else's repo.
17
+ */
18
+ export declare function corpusFromMap(files: CorpusFiles): Corpus;
package/dist/corpus.js ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A corpus as the importer sees it: its file paths (posix, relative to the corpus root) and a
3
+ * way to read one as text. The CLI builds one from a folder (`loaders/folder.ts`, which also
4
+ * keeps symlinks inside it); the browser builds one from a map of files (`corpusFromMap`).
5
+ * Assets are never read through it: the project lists them as copies, and each host copies
6
+ * the bytes its own way.
7
+ */
8
+ import { isPlainPath } from "@dogsbay/detect";
9
+ /**
10
+ * A corpus over a map of files; bytes are read as UTF-8. A key that is not a plain path
11
+ * (`../x`, `/etc/x`, `a/./b`, a backslash) is left out: the keys come from someone else's repo.
12
+ */
13
+ export function corpusFromMap(files) {
14
+ const decoder = new TextDecoder();
15
+ const paths = [...files.keys()].filter(isPlainPath).sort();
16
+ const listed = new Set(paths);
17
+ return {
18
+ paths,
19
+ text(path) {
20
+ if (!listed.has(path))
21
+ return undefined;
22
+ const v = files.get(path);
23
+ return v === undefined ? undefined : typeof v === "string" ? v : decoder.decode(v);
24
+ },
25
+ };
26
+ }
@@ -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";
@@ -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
+ }
@@ -4,5 +4,5 @@ export interface GitbookConfig {
4
4
  summary: string;
5
5
  redirects: Record<string, string>;
6
6
  }
7
- /** Read `.gitbook.yaml` from `dir` if present; else return defaults. */
8
- export declare function readGitbookConfig(dir: string): GitbookConfig;
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;
@@ -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 { existsSync, readFileSync } from "node:fs";
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` from `dir` if present; else return defaults. */
22
- export function readGitbookConfig(dir) {
23
- const p = join(dir, ".gitbook.yaml");
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 = YAML.parse(readFileSync(p, "utf-8"));
28
+ parsed = yaml.load(text);
29
29
  }
30
30
  catch {
31
31
  return { ...DEFAULTS };
@@ -0,0 +1,31 @@
1
+ import type { Corpus } from "../corpus.js";
2
+ import { type GitbookConfig } from "./config.js";
3
+ import { type GitbookLoadOptions, type GitbookLoadResult } from "./load.js";
4
+ export interface FolderCorpus extends Corpus {
5
+ /** The folder's absolute path. */
6
+ root: string;
7
+ /** A corpus path as an absolute path on disk. */
8
+ absolute(path: string): string;
9
+ }
10
+ /**
11
+ * List `dir` (skipping `node_modules` and `.git`) and read files from it on demand. A
12
+ * directory reached by two names is listed under the first one walked, so `first` names the
13
+ * folders to walk before the rest (the docs roots): a `docs -> content-src` link must list the
14
+ * pages under `docs/`, where `root: ./docs/` looks for them.
15
+ */
16
+ export declare function readFolder(dir: string, maxDepth?: number, first?: string[]): FolderCorpus;
17
+ /** Read `.gitbook.yaml` from `dir` if present; else return defaults. */
18
+ export declare function readGitbookConfig(dir: string): GitbookConfig;
19
+ /**
20
+ * Does this directory look like a GitBook corpus? The importer's own rule (a marker at the
21
+ * top, or subfolders with a SUMMARY.md), over the folder's top two levels, through the shared
22
+ * path detector (so an mdBook's `src/SUMMARY.md` beside `book.toml` is not claimed).
23
+ */
24
+ export declare function detectGitbook(sourceDir: string): boolean;
25
+ /**
26
+ * A GitBook folder as a corpus, its docs roots walked first: the spaces and each one's
27
+ * `.gitbook.yaml` `root:` are read from the top two levels, the same rule the import applies.
28
+ */
29
+ export declare function readGitbookFolder(sourceDir: string, space?: string): FolderCorpus;
30
+ /** `loadGitbookCorpus` over a folder. */
31
+ export declare function loadGitbook(sourceDir: string, opts?: GitbookLoadOptions): GitbookLoadResult;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Node only: a folder as a {@link Corpus}, and the CLI's on-disk detector. Everything else in
3
+ * the loader reads the corpus, so the CLI and the browser import run the same code.
4
+ *
5
+ * The walk keeps to the folder once links resolve, and visits each directory once: a symlink
6
+ * out of the repo read pages from elsewhere, and two links to `.` made the walk branch twice
7
+ * per level until ELOOP (about 2^40 visits). Text is read only when asked for.
8
+ */
9
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
10
+ import { join, sep } from "node:path";
11
+ import { detectGitbook as detectGitbookPaths, gitbookSpaces, isPlainPath, joinPath, normalizePath } from "@dogsbay/detect";
12
+ import { parseGitbookConfig } from "./config.js";
13
+ import { loadGitbookCorpus } from "./load.js";
14
+ /**
15
+ * List `dir` (skipping `node_modules` and `.git`) and read files from it on demand. A
16
+ * directory reached by two names is listed under the first one walked, so `first` names the
17
+ * folders to walk before the rest (the docs roots): a `docs -> content-src` link must list the
18
+ * pages under `docs/`, where `root: ./docs/` looks for them.
19
+ */
20
+ export function readFolder(dir, maxDepth = Infinity, first = []) {
21
+ const paths = [];
22
+ /** Each listed path's real path, for `contains`. */
23
+ const reals = new Map();
24
+ let realRoot;
25
+ try {
26
+ realRoot = realpathSync(dir);
27
+ }
28
+ catch {
29
+ realRoot = "";
30
+ }
31
+ if (realRoot) {
32
+ const seen = new Set([realRoot]);
33
+ const walk = (abs, rel, depth) => {
34
+ let entries;
35
+ try {
36
+ entries = readdirSync(abs).sort();
37
+ }
38
+ catch {
39
+ return;
40
+ }
41
+ for (const name of entries) {
42
+ if (name === "node_modules" || name === ".git")
43
+ continue;
44
+ const full = join(abs, name);
45
+ const path = rel ? `${rel}/${name}` : name;
46
+ if (!isPlainPath(path))
47
+ continue;
48
+ let real;
49
+ let st;
50
+ try {
51
+ real = realpathSync(full);
52
+ if (real !== realRoot && !real.startsWith(realRoot + sep))
53
+ continue;
54
+ st = statSync(real);
55
+ }
56
+ catch {
57
+ continue;
58
+ }
59
+ if (st.isDirectory()) {
60
+ if (seen.has(real) || depth + 1 > maxDepth)
61
+ continue;
62
+ seen.add(real);
63
+ walk(full, path, depth + 1);
64
+ }
65
+ else if (st.isFile()) {
66
+ paths.push(path);
67
+ reals.set(path, real);
68
+ }
69
+ }
70
+ };
71
+ for (const d of first) {
72
+ if (!d || !isPlainPath(d))
73
+ continue;
74
+ let real;
75
+ try {
76
+ real = realpathSync(join(dir, d));
77
+ if (!real.startsWith(realRoot + sep) || seen.has(real) || !statSync(real).isDirectory())
78
+ continue;
79
+ }
80
+ catch {
81
+ continue;
82
+ }
83
+ seen.add(real);
84
+ walk(join(dir, d), d, d.split("/").length);
85
+ }
86
+ walk(dir, "", 0);
87
+ }
88
+ const listed = new Set(paths);
89
+ const realDirs = new Map();
90
+ const realDir = (d) => {
91
+ if (!realDirs.has(d)) {
92
+ let r;
93
+ try {
94
+ r = realpathSync(join(dir, d));
95
+ }
96
+ catch {
97
+ r = undefined;
98
+ }
99
+ realDirs.set(d, r);
100
+ }
101
+ return realDirs.get(d);
102
+ };
103
+ return {
104
+ root: dir,
105
+ paths,
106
+ absolute: (path) => join(dir, path),
107
+ contains(path, d) {
108
+ const real = reals.get(path);
109
+ const root = realDir(d);
110
+ return !!real && !!root && (real === root || real.startsWith(root + sep));
111
+ },
112
+ text(path) {
113
+ if (!listed.has(path))
114
+ return undefined;
115
+ try {
116
+ return readFileSync(join(dir, path), "utf-8");
117
+ }
118
+ catch {
119
+ return undefined;
120
+ }
121
+ },
122
+ };
123
+ }
124
+ /** Read `.gitbook.yaml` from `dir` if present; else return defaults. */
125
+ export function readGitbookConfig(dir) {
126
+ const p = join(dir, ".gitbook.yaml");
127
+ return parseGitbookConfig(existsSync(p) ? readFileSync(p, "utf-8") : undefined);
128
+ }
129
+ /**
130
+ * Does this directory look like a GitBook corpus? The importer's own rule (a marker at the
131
+ * top, or subfolders with a SUMMARY.md), over the folder's top two levels, through the shared
132
+ * path detector (so an mdBook's `src/SUMMARY.md` beside `book.toml` is not claimed).
133
+ */
134
+ export function detectGitbook(sourceDir) {
135
+ try {
136
+ if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory())
137
+ return false;
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ const { paths } = readFolder(sourceDir, 1);
143
+ return detectGitbookPaths(paths) !== undefined && gitbookSpaces(paths).length > 0;
144
+ }
145
+ /**
146
+ * A GitBook folder as a corpus, its docs roots walked first: the spaces and each one's
147
+ * `.gitbook.yaml` `root:` are read from the top two levels, the same rule the import applies.
148
+ */
149
+ export function readGitbookFolder(sourceDir, space) {
150
+ const top = readFolder(sourceDir, 1);
151
+ const spaces = space ? [space] : gitbookSpaces(top.paths);
152
+ const roots = [];
153
+ for (const s of spaces.length ? spaces : ["."]) {
154
+ const spaceDir = normalizePath(s);
155
+ if (spaceDir === undefined)
156
+ continue;
157
+ const { root } = parseGitbookConfig(top.text(joinPath(spaceDir, ".gitbook.yaml")));
158
+ const docsDir = root && root !== "./" ? normalizePath(joinPath(spaceDir, root)) : spaceDir;
159
+ if (docsDir)
160
+ roots.push(docsDir);
161
+ }
162
+ return readFolder(sourceDir, Infinity, roots);
163
+ }
164
+ /** `loadGitbookCorpus` over a folder. */
165
+ export function loadGitbook(sourceDir, opts = {}) {
166
+ return loadGitbookCorpus(readGitbookFolder(sourceDir, opts.space), opts);
167
+ }