@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.
@@ -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
- filePath: string;
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
- /** Absolute docs directory (space dir + `.gitbook.yaml` `root:`). */
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 source); ≥2 ⇒ multi-space. */
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 source root). */
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
- /** Does this directory look like a GitBook corpus? */
31
- export declare function detectGitbook(sourceDir: string): boolean;
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;
@@ -1,163 +1,35 @@
1
- /**
2
- * GitBook corpus loader — discover the docs root, read `.gitbook.yaml`,
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
- * Directories (relative) that contain a SUMMARY.md — each is a space.
37
- *
38
- * If the ROOT itself has a SUMMARY.md, the corpus is single-space and we
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 findSpaces(sourceDir) {
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 seen = new Set();
51
- const consider = (dir) => {
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
- const full = join(sourceDir, name);
72
- try {
73
- if (statSync(full).isDirectory())
74
- consider(full);
75
- }
76
- catch {
77
- /* ignore */
78
- }
79
- }
80
- return out;
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 loadGitbook(sourceDir, opts = {}) {
28
+ export function loadGitbookCorpus(corpus, opts = {}) {
157
29
  const hrefPrefix = opts.hrefPrefix ?? "";
158
- const allSpaces = findSpaces(sourceDir);
159
- // Which spaces to load: an explicit --space, else all detected (or the
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 = space === "." ? sourceDir : join(sourceDir, space);
171
- if (!existsSync(spaceDir))
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 = readGitbookConfig(spaceDir);
174
- // `root:` relocates the docs dir within the space.
175
- const docsDir = config.root && config.root !== "./" ? join(spaceDir, config.root) : spaceDir;
176
- // `root:` comes from the repo being imported: it may not lead out of it.
177
- if (!insideDir(docsDir, sourceDir)) {
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 && space !== "." ? `${space}/` : "";
59
+ const slugPrefix = multi && spaceDir ? `${spaceDir}/` : "";
182
60
  assetRoots.push({ dir: docsDir, prefix: slugPrefix });
183
- for (const p of walkPages(docsDir, docsDir)) {
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 = join(docsDir, config.summary);
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 (existsSync(summaryPath)) {
190
- const parsed = parseSummary(readFileSync(summaryPath, "utf-8"), navPrefix);
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
- // space, so BOTH get the space slug prefix in a multi-space import —
208
- // else a redirect in space `docs` would point at an unprefixed target
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
- const val = slugPrefix ? `${slugPrefix}${v.replace(/^\.\//, "")}` : v;
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(basename(p.slug)), href: `/${p.slug}` }))
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
- /** Normalize a GitBook page path to a Dogsbay slug (README → index). */
26
- export declare function pathToSlug(relPath: string): string;
27
- export declare function parseSummary(content: string, hrefPrefix?: string): ParsedSummary;
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;
@@ -1,6 +1,16 @@
1
- /** Normalize a GitBook page path to a Dogsbay slug (README → index). */
2
- export function pathToSlug(relPath) {
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>;
@@ -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 declare function parseGitbook(src: string): ParseGitbookResult;
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 { markdown: translatedBody, ledger } = gitbookToDogsbayMd(body);
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
- return { tree, frontmatter: frontmatter ?? {}, markdown, body: translatedBody, ledger };
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;