@dogsbay/format-asciidoc 0.2.0-beta.48

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.
Files changed (54) hide show
  1. package/README.md +40 -0
  2. package/dist/cli.d.ts +3 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +239 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/detect.d.ts +42 -0
  7. package/dist/detect.d.ts.map +1 -0
  8. package/dist/detect.js +56 -0
  9. package/dist/detect.js.map +1 -0
  10. package/dist/index.d.ts +10 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +9 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/inline-walker.d.ts +17 -0
  15. package/dist/inline-walker.d.ts.map +1 -0
  16. package/dist/inline-walker.js +167 -0
  17. package/dist/inline-walker.js.map +1 -0
  18. package/dist/loaders/antora.d.ts +3 -0
  19. package/dist/loaders/antora.d.ts.map +1 -0
  20. package/dist/loaders/antora.js +254 -0
  21. package/dist/loaders/antora.js.map +1 -0
  22. package/dist/loaders/asciibinder.d.ts +3 -0
  23. package/dist/loaders/asciibinder.d.ts.map +1 -0
  24. package/dist/loaders/asciibinder.js +206 -0
  25. package/dist/loaders/asciibinder.js.map +1 -0
  26. package/dist/loaders/master-adoc.d.ts +3 -0
  27. package/dist/loaders/master-adoc.d.ts.map +1 -0
  28. package/dist/loaders/master-adoc.js +168 -0
  29. package/dist/loaders/master-adoc.js.map +1 -0
  30. package/dist/loaders/plain.d.ts +3 -0
  31. package/dist/loaders/plain.d.ts.map +1 -0
  32. package/dist/loaders/plain.js +61 -0
  33. package/dist/loaders/plain.js.map +1 -0
  34. package/dist/loaders/types.d.ts +88 -0
  35. package/dist/loaders/types.d.ts.map +1 -0
  36. package/dist/loaders/types.js +2 -0
  37. package/dist/loaders/types.js.map +1 -0
  38. package/dist/loaders/util.d.ts +15 -0
  39. package/dist/loaders/util.d.ts.map +1 -0
  40. package/dist/loaders/util.js +49 -0
  41. package/dist/loaders/util.js.map +1 -0
  42. package/dist/parse.d.ts +110 -0
  43. package/dist/parse.d.ts.map +1 -0
  44. package/dist/parse.js +116 -0
  45. package/dist/parse.js.map +1 -0
  46. package/dist/tree.d.ts +2 -0
  47. package/dist/tree.d.ts.map +1 -0
  48. package/dist/tree.js +2 -0
  49. package/dist/tree.js.map +1 -0
  50. package/dist/walker.d.ts +8 -0
  51. package/dist/walker.d.ts.map +1 -0
  52. package/dist/walker.js +193 -0
  53. package/dist/walker.js.map +1 -0
  54. package/package.json +53 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Antora corpus loader (component mode).
3
+ *
4
+ * Detects: presence of `antora.yml` at the source root. Reads the
5
+ * component manifest (name + version + nav file list + asciidoc
6
+ * attribute defaults), walks `modules/<name>/pages/**\/*.adoc`, and
7
+ * builds nav from the referenced `nav.adoc` files.
8
+ *
9
+ * **Scope of this MVP — what's covered:**
10
+ *
11
+ * - Single-component mode (one antora.yml).
12
+ * - All modules under `modules/<name>/pages/` — ROOT pages get no
13
+ * slug prefix; other modules' pages get a `<module-name>/` prefix.
14
+ * - nav.adoc parsing: depth-based list items (`*`, `**`, `***`), three
15
+ * shapes — `* xref:path[label]`, `** xref:path[]` (empty brackets →
16
+ * label from filename), `* Text` (no xref → group header).
17
+ * - Per-component attribute defaults from `antora.yml`'s
18
+ * `asciidoc.attributes:` block are surfaced on every
19
+ * `PageSource.attributes`.
20
+ * - Component name + version surfaced on `LoaderResult.component`.
21
+ *
22
+ * **Deliberately out of scope (deferred):**
23
+ *
24
+ * - Playbook mode (`antora-playbook.yml`) — pulls in git-resolved
25
+ * sources + multi-component aggregation. Component mode covers the
26
+ * common single-repo case; playbook mode is a follow-up.
27
+ * - Cross-module xref resolution (`xref:module:path[label]`). The
28
+ * engine emits the xref as text or `{% include %}` per its rules;
29
+ * format-astro's downstream link audit will surface the unresolved
30
+ * refs.
31
+ * - Version axis emission. The component's version is surfaced as
32
+ * data but not wired into format-astro's version-axis machinery
33
+ * (per plans/asciidoc-corpus-loaders.md open question — same
34
+ * deferral as asciibinder distros).
35
+ *
36
+ * Sample antora.yml shape:
37
+ *
38
+ * name: product
39
+ * title: My Product
40
+ * version: '2.0' # null / ~ also valid (unversioned)
41
+ * nav:
42
+ * - modules/ROOT/nav.adoc
43
+ * - modules/admin/nav.adoc
44
+ * asciidoc:
45
+ * attributes:
46
+ * page-component-name: product
47
+ *
48
+ * Sample nav.adoc:
49
+ *
50
+ * * xref:index.adoc[Introduction]
51
+ * * Getting Started
52
+ * ** xref:install.adoc[]
53
+ * ** xref:configure.adoc[Configuration]
54
+ *
55
+ * See plans/asciidoc-corpus-loaders.md "Antora" section.
56
+ */
57
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
58
+ import { basename, join, relative } from "node:path";
59
+ import YAML from "yaml";
60
+ import { prettifyName } from "./util.js";
61
+ const ANTORA_YML = "antora.yml";
62
+ const ROOT_MODULE = "ROOT";
63
+ function walkPagesUnderModule(modulesDir, moduleName) {
64
+ const pagesDir = join(modulesDir, moduleName, "pages");
65
+ if (!existsSync(pagesDir))
66
+ return [];
67
+ const out = [];
68
+ function walk(dir) {
69
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
70
+ const full = join(dir, entry.name);
71
+ if (entry.isDirectory()) {
72
+ walk(full);
73
+ }
74
+ else if (entry.name.endsWith(".adoc")) {
75
+ const relFromPages = relative(pagesDir, full).replace(/\\/g, "/");
76
+ const pageSlug = relFromPages.replace(/\.adoc$/, "");
77
+ // ROOT pages get no module prefix; other modules get one.
78
+ const slug = moduleName === ROOT_MODULE ? pageSlug : `${moduleName}/${pageSlug}`;
79
+ out.push({ filePath: full, slug });
80
+ }
81
+ }
82
+ }
83
+ walk(pagesDir);
84
+ return out;
85
+ }
86
+ function listModules(sourceDir) {
87
+ const modulesDir = join(sourceDir, "modules");
88
+ if (!existsSync(modulesDir))
89
+ return [];
90
+ try {
91
+ return readdirSync(modulesDir, { withFileTypes: true })
92
+ .filter((e) => e.isDirectory())
93
+ .map((e) => e.name);
94
+ }
95
+ catch {
96
+ return [];
97
+ }
98
+ }
99
+ /**
100
+ * Parse a nav.adoc file into a NavItem[] tree.
101
+ *
102
+ * Each line of the form `*+ <content>` is a nav item. The number of
103
+ * leading `*` characters is the depth (1-based). Content is either:
104
+ * - `xref:<path>.adoc[<label>]` → leaf; label may be empty (use filename)
105
+ * - `xref:<module>:<path>.adoc[<label>]` → leaf in another module
106
+ * - plain text → group header (no link)
107
+ *
108
+ * Algorithm: stack-of-parents per depth. Lines that aren't `*+ ...`
109
+ * are ignored (blank lines, `.Section` headings, prose).
110
+ *
111
+ * `moduleName` is the module the nav lives in — xrefs without an
112
+ * explicit module prefix resolve under this module's pages/.
113
+ * `hrefPrefix` prepends every leaf href.
114
+ */
115
+ function parseNavAdoc(content, moduleName, hrefPrefix) {
116
+ const root = [];
117
+ // Stack[d] = the children-array we should push into for a depth-(d+1)
118
+ // item. stack[0] is `root` (depth 1 items go here).
119
+ const stack = [root];
120
+ const lineRe = /^(\*+)\s+(.+)$/;
121
+ for (const rawLine of content.split("\n")) {
122
+ const line = rawLine.replace(/\s+$/, "");
123
+ const m = line.match(lineRe);
124
+ if (!m)
125
+ continue;
126
+ const depth = m[1].length;
127
+ const text = m[2].trim();
128
+ const item = parseNavItem(text, moduleName, hrefPrefix);
129
+ // Find the parent array — if depth exceeds current stack length,
130
+ // we're going deeper than any actual parent; clamp to current top.
131
+ const parentIdx = Math.min(depth - 1, stack.length - 1);
132
+ const parent = stack[parentIdx];
133
+ parent.push(item);
134
+ // This item becomes the parent for depth+1 items. Allocate its
135
+ // children array up-front (so we can push later); if it stays
136
+ // empty, we strip it before returning.
137
+ if (!item.children)
138
+ item.children = [];
139
+ // Trim deeper-than-current entries; replace stack[depth] with our children.
140
+ stack.length = depth;
141
+ stack.push(item.children);
142
+ }
143
+ // Clean up: drop empty `children: []` arrays so leaves don't look
144
+ // like empty groups to downstream consumers.
145
+ function strip(items) {
146
+ for (const it of items) {
147
+ if (it.children && it.children.length === 0) {
148
+ delete it.children;
149
+ }
150
+ else if (it.children) {
151
+ strip(it.children);
152
+ }
153
+ }
154
+ }
155
+ strip(root);
156
+ return root;
157
+ }
158
+ function parseNavItem(text, moduleName, hrefPrefix) {
159
+ // xref:<path>[<label>] or xref:<module>:<path>[<label>]
160
+ const xrefRe = /^xref:(?:([^:\s]+):)?([^\[\s]+)\[([^\]]*)\]$/;
161
+ const m = text.match(xrefRe);
162
+ if (!m) {
163
+ // Plain text — group header.
164
+ return { label: text };
165
+ }
166
+ const xrefModule = m[1] || moduleName;
167
+ const xrefPath = m[2].replace(/\.adoc$/, "");
168
+ const label = m[3].trim() || prettifyName(basename(xrefPath));
169
+ const slug = xrefModule === ROOT_MODULE ? xrefPath : `${xrefModule}/${xrefPath}`;
170
+ return { label, href: `${hrefPrefix}/${slug}` };
171
+ }
172
+ export const antoraLoader = {
173
+ name: "antora",
174
+ detect(sourceDir) {
175
+ if (!existsSync(sourceDir))
176
+ return false;
177
+ try {
178
+ const st = statSync(sourceDir);
179
+ if (!st.isDirectory())
180
+ return false;
181
+ return existsSync(join(sourceDir, ANTORA_YML));
182
+ }
183
+ catch {
184
+ return false;
185
+ }
186
+ },
187
+ async load(sourceDir, opts) {
188
+ const ymlPath = join(sourceDir, ANTORA_YML);
189
+ if (!existsSync(ymlPath)) {
190
+ throw new Error(`antora loader: ${ANTORA_YML} not found in ${sourceDir}`);
191
+ }
192
+ let parsed;
193
+ try {
194
+ parsed = YAML.parse(readFileSync(ymlPath, "utf-8"));
195
+ }
196
+ catch (err) {
197
+ const msg = err instanceof Error ? err.message : String(err);
198
+ throw new Error(`antora loader: cannot parse ${ANTORA_YML}: ${msg}`);
199
+ }
200
+ if (!parsed || typeof parsed !== "object" || !parsed.name) {
201
+ throw new Error(`antora loader: ${ANTORA_YML} must declare at least a 'name' field`);
202
+ }
203
+ const hrefPrefix = opts.hrefPrefix ?? "";
204
+ const componentAttrs = parsed.asciidoc?.attributes ?? {};
205
+ // Walk every module's pages/ to assemble PageSources. We don't
206
+ // restrict to modules listed in nav: — a corpus can have pages
207
+ // not referenced by nav (orphans), and the audit will surface
208
+ // them; the loader's job is to enumerate them all.
209
+ const modulesDir = join(sourceDir, "modules");
210
+ const moduleNames = listModules(sourceDir);
211
+ const pages = [];
212
+ for (const moduleName of moduleNames) {
213
+ for (const { filePath, slug } of walkPagesUnderModule(modulesDir, moduleName)) {
214
+ const page = { filePath, slug };
215
+ if (Object.keys(componentAttrs).length > 0) {
216
+ page.attributes = { ...componentAttrs };
217
+ }
218
+ pages.push(page);
219
+ }
220
+ }
221
+ // Build nav by parsing each referenced nav.adoc file. Concatenate
222
+ // multiple nav files at the top level — that's Antora's
223
+ // convention (each nav.adoc is a "section" of the overall nav).
224
+ const nav = [];
225
+ for (const navPath of parsed.nav ?? []) {
226
+ const navFullPath = join(sourceDir, navPath);
227
+ if (!existsSync(navFullPath)) {
228
+ // eslint-disable-next-line no-console
229
+ console.warn(`antora loader: nav file not found, skipping: ${navPath}`);
230
+ continue;
231
+ }
232
+ // Module name from the nav file's path — `modules/<name>/nav.adoc`.
233
+ const navRelative = relative(modulesDir, navFullPath).replace(/\\/g, "/");
234
+ const moduleName = navRelative.split("/")[0] ?? ROOT_MODULE;
235
+ try {
236
+ const content = readFileSync(navFullPath, "utf-8");
237
+ nav.push(...parseNavAdoc(content, moduleName, hrefPrefix));
238
+ }
239
+ catch (err) {
240
+ const msg = err instanceof Error ? err.message : String(err);
241
+ // eslint-disable-next-line no-console
242
+ console.warn(`antora loader: cannot read ${navPath}: ${msg}`);
243
+ }
244
+ }
245
+ const result = { pages, nav };
246
+ result.component = {
247
+ name: parsed.name,
248
+ ...(parsed.title ? { title: parsed.title } : {}),
249
+ ...(parsed.version ? { version: String(parsed.version) } : {}),
250
+ };
251
+ return result;
252
+ },
253
+ };
254
+ //# sourceMappingURL=antora.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"antora.js","sourceRoot":"","sources":["../../src/loaders/antora.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { CorpusLoader } from "./types.js";
2
+ export declare const asciibinderLoader: CorpusLoader;
3
+ //# sourceMappingURL=asciibinder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asciibinder.d.ts","sourceRoot":"","sources":["../../src/loaders/asciibinder.ts"],"names":[],"mappings":"AA8CA,OAAO,KAAK,EACV,YAAY,EAIb,MAAM,YAAY,CAAC;AA+GpB,eAAO,MAAM,iBAAiB,EAAE,YAwF/B,CAAC"}
@@ -0,0 +1,206 @@
1
+ /**
2
+ * AsciiBinder corpus loader (OpenShift docs convention).
3
+ *
4
+ * Detects: presence of `_topic_maps/_topic_map.yml` at the source
5
+ * root. Optionally consumes a sibling `_topic_maps/_distro_map.yml`
6
+ * to surface the corpus's distro set on the LoaderResult.
7
+ *
8
+ * Topology shape (YAML):
9
+ *
10
+ * - Name: Welcome
11
+ * Dir: welcome
12
+ * Topics:
13
+ * - Name: About OpenShift
14
+ * File: index
15
+ * - Name: Architecture
16
+ * File: architecture
17
+ * Distros: openshift-enterprise,openshift-online
18
+ * - Name: Getting Started
19
+ * Dir: getting-started
20
+ * Topics:
21
+ * - ...
22
+ *
23
+ * Translation:
24
+ * - Each entry is either a **group** (has `Topics`, no `File`) or a
25
+ * **leaf** (has `File`, no `Topics`). The plan allows both fields
26
+ * in theory, but the real-world OpenShift corpora always pick one.
27
+ * - `Dir` accumulates down the topic tree to form the page slug:
28
+ * a leaf at `Topics[0].Topics[1]` with the chain
29
+ * `Dir: welcome` → `Dir: gettingstarted` → `File: quickstart`
30
+ * becomes slug `welcome/gettingstarted/quickstart`.
31
+ * - `Distros` on a leaf is parsed (comma-separated) and surfaced as
32
+ * `PageSource.attributes.distros: string[]`. The aggregate union is
33
+ * also stashed on `LoaderResult.distros` for future distro-axis
34
+ * emission (Phase 9b doesn't yet wire that through format-astro;
35
+ * see plans/asciidoc-corpus-loaders.md open question #1).
36
+ *
37
+ * Missing files (topic references a file that's not on disk) get
38
+ * warn-and-skip semantics, matching the master-adoc loader. The
39
+ * audit catches the wider issue.
40
+ *
41
+ * See plans/asciidoc-corpus-loaders.md "AsciiBinder" section.
42
+ */
43
+ import { existsSync, readFileSync, statSync } from "node:fs";
44
+ import { join } from "node:path";
45
+ import YAML from "yaml";
46
+ const TOPIC_MAP_PATH = "_topic_maps/_topic_map.yml";
47
+ const DISTRO_MAP_PATH = "_topic_maps/_distro_map.yml";
48
+ /**
49
+ * Walk a TopicEntry[] subtree. Pushes leaves into `pages` + matching
50
+ * NavItem links into `nav`; pushes group entries (entries with
51
+ * `Topics`) as nested NavItem groups whose children come from a
52
+ * recursive walk.
53
+ *
54
+ * `parentDirSegments` is the directory chain accumulated from
55
+ * ancestor `Dir:` fields; concatenated with the entry's own `Dir:`
56
+ * (if any) and the leaf's `File:` to form the slug.
57
+ *
58
+ * `distroAcc` is the set of distros encountered anywhere in the
59
+ * tree — surfaced on LoaderResult.distros.
60
+ */
61
+ function walkTopics(entries, parentDirSegments, pages, hrefPrefix, sourceRoot, distroAcc) {
62
+ const nav = [];
63
+ for (const entry of entries) {
64
+ const label = entry.Name ?? "Untitled";
65
+ const dirSegments = entry.Dir
66
+ ? [...parentDirSegments, entry.Dir]
67
+ : parentDirSegments;
68
+ if (entry.File !== undefined) {
69
+ const slug = [...dirSegments, entry.File].filter(Boolean).join("/");
70
+ const filePath = join(sourceRoot, `${slug}.adoc`);
71
+ if (!existsSync(filePath)) {
72
+ // eslint-disable-next-line no-console
73
+ console.warn(`asciibinder loader: topic_map references missing file, skipping: ${slug}.adoc`);
74
+ continue;
75
+ }
76
+ const distros = parseDistros(entry.Distros);
77
+ for (const d of distros)
78
+ distroAcc.add(d);
79
+ const page = { filePath, slug };
80
+ if (distros.length > 0) {
81
+ page.attributes = { distros };
82
+ }
83
+ pages.push(page);
84
+ nav.push({ label, href: `${hrefPrefix}/${slug}` });
85
+ continue;
86
+ }
87
+ if (entry.Topics && entry.Topics.length > 0) {
88
+ const children = walkTopics(entry.Topics, dirSegments, pages, hrefPrefix, sourceRoot, distroAcc);
89
+ nav.push({ label, children });
90
+ continue;
91
+ }
92
+ // Entry has neither File nor Topics — malformed but tolerated.
93
+ // Emit a stub nav entry so the user notices the gap rather than
94
+ // a silent skip.
95
+ nav.push({ label });
96
+ }
97
+ return nav;
98
+ }
99
+ function parseDistros(raw) {
100
+ if (!raw || typeof raw !== "string")
101
+ return [];
102
+ return raw
103
+ .split(",")
104
+ .map((s) => s.trim())
105
+ .filter(Boolean);
106
+ }
107
+ /**
108
+ * Read the optional _distro_map.yml to learn the corpus's full
109
+ * distro set. Treated as informational — we surface the union of
110
+ * what the topic map declares AND what the distro map declares,
111
+ * so consumers see every distro name the source author cared about.
112
+ */
113
+ function readDistroMap(sourceDir) {
114
+ const path = join(sourceDir, DISTRO_MAP_PATH);
115
+ if (!existsSync(path))
116
+ return [];
117
+ try {
118
+ const yml = YAML.parse(readFileSync(path, "utf-8"));
119
+ if (yml && typeof yml === "object" && !Array.isArray(yml)) {
120
+ return Object.keys(yml);
121
+ }
122
+ }
123
+ catch {
124
+ // Malformed map — ignore, fall back to topic-map-derived distros only.
125
+ }
126
+ return [];
127
+ }
128
+ export const asciibinderLoader = {
129
+ name: "asciibinder",
130
+ detect(sourceDir) {
131
+ if (!existsSync(sourceDir))
132
+ return false;
133
+ try {
134
+ const st = statSync(sourceDir);
135
+ if (!st.isDirectory())
136
+ return false;
137
+ return existsSync(join(sourceDir, TOPIC_MAP_PATH));
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ },
143
+ async load(sourceDir, opts) {
144
+ const topicMapPath = join(sourceDir, TOPIC_MAP_PATH);
145
+ if (!existsSync(topicMapPath)) {
146
+ throw new Error(`asciibinder loader: ${TOPIC_MAP_PATH} not found in ${sourceDir}`);
147
+ }
148
+ // AsciiBinder's `_topic_map.yml` is a stream of YAML documents
149
+ // separated by `---` (each document is one top-level topic
150
+ // group). The file's own header documents this:
151
+ // "--- <= Record delimiter"
152
+ // Real-world corpora (OpenShift docs etc.) rely on it. We
153
+ // accept either shape:
154
+ // (a) the conventional multi-doc stream — each `---` group
155
+ // contributes one entry to the topic list, OR
156
+ // (b) a single doc with a top-level list — what our earlier
157
+ // tests / synthesized fixtures used.
158
+ let parsed;
159
+ try {
160
+ const raw = readFileSync(topicMapPath, "utf-8");
161
+ const docs = YAML.parseAllDocuments(raw);
162
+ // parseAllDocuments doesn't throw on bad YAML — it attaches
163
+ // errors to each Document. Surface the first one with a
164
+ // clear message.
165
+ for (const doc of docs) {
166
+ if (doc.errors && doc.errors.length > 0) {
167
+ throw new Error(doc.errors[0].message);
168
+ }
169
+ }
170
+ // Skip empty documents (a trailing `---` produces a null one)
171
+ // and unwrap each Document to its JS value.
172
+ const values = docs
173
+ .map((d) => d.toJS())
174
+ .filter((v) => v !== null && v !== undefined);
175
+ if (values.length === 1 && Array.isArray(values[0])) {
176
+ // Shape (b) — single doc that already wraps the list.
177
+ parsed = values[0];
178
+ }
179
+ else {
180
+ // Shape (a) — each doc is a topic-group object; flatten
181
+ // into a list. Defensive: if any single doc is itself a
182
+ // list (mixed shape), spread it.
183
+ parsed = values.flatMap((v) => (Array.isArray(v) ? v : [v]));
184
+ }
185
+ }
186
+ catch (err) {
187
+ const msg = err instanceof Error ? err.message : String(err);
188
+ throw new Error(`asciibinder loader: cannot parse ${TOPIC_MAP_PATH}: ${msg}`);
189
+ }
190
+ if (!Array.isArray(parsed) || parsed.length === 0) {
191
+ throw new Error(`asciibinder loader: ${TOPIC_MAP_PATH} must contain at least one topic-group entry ` +
192
+ `(either as a single top-level list, or as a stream of YAML documents separated by \`---\`)`);
193
+ }
194
+ const hrefPrefix = opts.hrefPrefix ?? "";
195
+ const pages = [];
196
+ const distroAcc = new Set();
197
+ const nav = walkTopics(parsed, [], pages, hrefPrefix, sourceDir, distroAcc);
198
+ for (const d of readDistroMap(sourceDir))
199
+ distroAcc.add(d);
200
+ const result = { pages, nav };
201
+ if (distroAcc.size > 0)
202
+ result.distros = [...distroAcc].sort();
203
+ return result;
204
+ },
205
+ };
206
+ //# sourceMappingURL=asciibinder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"asciibinder.js","sourceRoot":"","sources":["../../src/loaders/asciibinder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,IAAI,MAAM,MAAM,CAAC;AASxB,MAAM,cAAc,GAAG,4BAA4B,CAAC;AACpD,MAAM,eAAe,GAAG,6BAA6B,CAAC;AAUtD;;;;;;;;;;;;GAYG;AACH,SAAS,UAAU,CACjB,OAAqB,EACrB,iBAA2B,EAC3B,KAAmB,EACnB,UAAkB,EAClB,UAAkB,EAClB,SAAsB;IAEtB,MAAM,GAAG,GAAc,EAAE,CAAC;IAC1B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,IAAI,UAAU,CAAC;QACvC,MAAM,WAAW,GAAG,KAAK,CAAC,GAAG;YAC3B,CAAC,CAAC,CAAC,GAAG,iBAAiB,EAAE,KAAK,CAAC,GAAG,CAAC;YACnC,CAAC,CAAC,iBAAiB,CAAC;QAEtB,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,GAAG,CAAC,GAAG,WAAW,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpE,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;YAClD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAC1B,sCAAsC;gBACtC,OAAO,CAAC,IAAI,CACV,oEAAoE,IAAI,OAAO,CAChF,CAAC;gBACF,SAAS;YACX,CAAC;YACD,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5C,KAAK,MAAM,CAAC,IAAI,OAAO;gBAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;YAC1C,MAAM,IAAI,GAAe,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YAC5C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACvB,IAAI,CAAC,UAAU,GAAG,EAAE,OAAO,EAAE,CAAC;YAChC,CAAC;YACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACjB,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,UAAU,IAAI,IAAI,EAAE,EAAE,CAAC,CAAC;YACnD,SAAS;QACX,CAAC;QAED,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5C,MAAM,QAAQ,GAAG,UAAU,CACzB,KAAK,CAAC,MAAM,EACZ,WAAW,EACX,KAAK,EACL,UAAU,EACV,UAAU,EACV,SAAS,CACV,CAAC;YACF,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;YAC9B,SAAS;QACX,CAAC;QAED,+DAA+D;QAC/D,gEAAgE;QAChE,iBAAiB;QACjB,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IACtB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,YAAY,CAAC,GAAuB;IAC3C,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IAC/C,OAAO,GAAG;SACP,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED;;;;;GAKG;AACH,SAAS,aAAa,CAAC,SAAiB;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;IAC9C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;QACpD,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1D,OAAO,MAAM,CAAC,IAAI,CAAC,GAA8B,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;IACzE,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAiB;IAC7C,IAAI,EAAE,aAAa;IAEnB,MAAM,CAAC,SAAiB;QACtB,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,KAAK,CAAC;QACzC,IAAI,CAAC;YACH,MAAM,EAAE,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;YAC/B,IAAI,CAAC,EAAE,CAAC,WAAW,EAAE;gBAAE,OAAO,KAAK,CAAC;YACpC,OAAO,UAAU,CAAC,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC,CAAC;QACrD,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,SAAiB,EAAE,IAAgB;QAC5C,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;QACrD,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;YAC9B,MAAM,IAAI,KAAK,CACb,uBAAuB,cAAc,iBAAiB,SAAS,EAAE,CAClE,CAAC;QACJ,CAAC;QAED,+DAA+D;QAC/D,2DAA2D;QAC3D,gDAAgD;QAChD,8BAA8B;QAC9B,0DAA0D;QAC1D,uBAAuB;QACvB,6DAA6D;QAC7D,oDAAoD;QACpD,8DAA8D;QAC9D,2CAA2C;QAC3C,IAAI,MAAe,CAAC;QACpB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;YAChD,MAAM,IAAI,GAAG,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;YACzC,4DAA4D;YAC5D,wDAAwD;YACxD,iBAAiB;YACjB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;gBACvB,IAAI,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACxC,MAAM,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;gBACzC,CAAC;YACH,CAAC;YACD,8DAA8D;YAC9D,4CAA4C;YAC5C,MAAM,MAAM,GAAG,IAAI;iBAChB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;iBACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS,CAAC,CAAC;YAChD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;gBACpD,sDAAsD;gBACtD,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACrB,CAAC;iBAAM,CAAC;gBACN,wDAAwD;gBACxD,wDAAwD;gBACxD,iCAAiC;gBACjC,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAC/D,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,MAAM,IAAI,KAAK,CAAC,oCAAoC,cAAc,KAAK,GAAG,EAAE,CAAC,CAAC;QAChF,CAAC;QAED,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAClD,MAAM,IAAI,KAAK,CACb,uBAAuB,cAAc,+CAA+C;gBACpF,4FAA4F,CAC7F,CAAC;QACJ,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC;QACzC,MAAM,KAAK,GAAiB,EAAE,CAAC;QAC/B,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;QACpC,MAAM,GAAG,GAAG,UAAU,CACpB,MAAsB,EACtB,EAAE,EACF,KAAK,EACL,UAAU,EACV,SAAS,EACT,SAAS,CACV,CAAC;QAEF,KAAK,MAAM,CAAC,IAAI,aAAa,CAAC,SAAS,CAAC;YAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAE3D,MAAM,MAAM,GAAiB,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;QAC5C,IAAI,SAAS,CAAC,IAAI,GAAG,CAAC;YAAE,MAAM,CAAC,OAAO,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,EAAE,CAAC;QAC/D,OAAO,MAAM,CAAC;IAChB,CAAC;CACF,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { CorpusLoader } from "./types.js";
2
+ export declare const masterAdocLoader: CorpusLoader;
3
+ //# sourceMappingURL=master-adoc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"master-adoc.d.ts","sourceRoot":"","sources":["../../src/loaders/master-adoc.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EACV,YAAY,EAIb,MAAM,YAAY,CAAC;AAmDpB,eAAO,MAAM,gBAAgB,EAAE,YAiG9B,CAAC"}
@@ -0,0 +1,168 @@
1
+ /**
2
+ * AAP master.adoc corpus loader.
3
+ *
4
+ * AAP (Ansible Automation Platform) and adjacent Red Hat docs use a
5
+ * convention where each "title" is a directory containing a single
6
+ * `master.adoc` file. That master.adoc is pure structure — typically
7
+ * an `:attribute:` block plus a sequence of `include::` directives
8
+ * pointing at module `.adoc` files (usually shared across titles
9
+ * under a sibling `modules/` tree).
10
+ *
11
+ * Phase 9a — first non-plain corpus loader, simplest topology
12
+ * (no central topo file like AsciiBinder's `_topic_map.yml`).
13
+ *
14
+ * Behaviour:
15
+ * - Walk the source dir for `master.adoc` files (recursive).
16
+ * - For each master, parse its `include::` directives in order;
17
+ * each include becomes a `PageSource` (filePath resolved relative
18
+ * to the master, slug derived from the resolved path so siblings
19
+ * sharing a module map to the same slug — last-write-wins is
20
+ * fine; the corpus is conceptually merged).
21
+ * - Build a per-title nav group: the title comes from the master's
22
+ * parent directory name (prettified); children are the includes
23
+ * in source order, labelled by their filename (prettified).
24
+ * - Master.adoc files themselves do NOT become pages — they're
25
+ * pure structure, not user-facing content.
26
+ *
27
+ * Lineage: md2md's `convert-all-titles.sh` is the shell prototype
28
+ * of this discovery pattern.
29
+ *
30
+ * See plans/asciidoc-corpus-loaders.md "AAP master.adoc" section.
31
+ */
32
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
33
+ import { basename, dirname, join, relative, resolve } from "node:path";
34
+ import { prettifyName } from "./util.js";
35
+ function walkMasterAdocs(dir, out = []) {
36
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
37
+ const full = join(dir, entry.name);
38
+ if (entry.isDirectory()) {
39
+ walkMasterAdocs(full, out);
40
+ }
41
+ else if (entry.name === "master.adoc") {
42
+ out.push(full);
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ /**
48
+ * Extract include:: directives from an AsciiDoc source.
49
+ *
50
+ * Matches the shape `include::path[attr=value,...]`. Multi-line
51
+ * include blocks are not supported (the engine handles those at
52
+ * convert time; the loader only needs to enumerate top-level
53
+ * module references). Returns `null` for the `leveloffset` attribute
54
+ * when absent.
55
+ */
56
+ function extractIncludes(adoc) {
57
+ const out = [];
58
+ const re = /^include::([^\[\s]+)\[([^\]]*)\]\s*$/gm;
59
+ for (const m of adoc.matchAll(re)) {
60
+ const path = m[1];
61
+ const attrs = m[2];
62
+ let leveloffset;
63
+ const lvl = /leveloffset\s*=\s*([+-]?\d+)/.exec(attrs);
64
+ if (lvl)
65
+ leveloffset = Number(lvl[1]);
66
+ out.push({ path, leveloffset });
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * Resolve an include path against the directory containing the
72
+ * including file. AsciiDoc semantics: paths are relative to the
73
+ * source file, NOT to the document root. `..` and `.` segments are
74
+ * normalised by `node:path/resolve`.
75
+ */
76
+ function resolveIncludePath(includingFile, includePath) {
77
+ return resolve(dirname(includingFile), includePath);
78
+ }
79
+ export const masterAdocLoader = {
80
+ name: "master-adoc",
81
+ detect(sourceDir) {
82
+ if (!existsSync(sourceDir))
83
+ return false;
84
+ try {
85
+ const st = statSync(sourceDir);
86
+ if (!st.isDirectory())
87
+ return false;
88
+ // Walk subtree for any master.adoc — bail as soon as we find one.
89
+ function probe(dir) {
90
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
91
+ if (entry.isDirectory()) {
92
+ if (probe(join(dir, entry.name)))
93
+ return true;
94
+ }
95
+ else if (entry.name === "master.adoc") {
96
+ return true;
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+ return probe(sourceDir);
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ },
107
+ async load(sourceDir, opts) {
108
+ if (!existsSync(sourceDir)) {
109
+ throw new Error(`Source directory not found: ${sourceDir}`);
110
+ }
111
+ const hrefPrefix = opts.hrefPrefix ?? "";
112
+ const masters = walkMasterAdocs(sourceDir).sort();
113
+ if (masters.length === 0) {
114
+ throw new Error(`master-adoc loader: no master.adoc files found under ${sourceDir}`);
115
+ }
116
+ // Track unique PageSources by resolved filePath so a module
117
+ // included from multiple titles produces ONE page (last-write-wins
118
+ // for the leveloffset / attributes hint — typically all titles use
119
+ // the same shape anyway).
120
+ const pageByPath = new Map();
121
+ const navGroups = [];
122
+ for (const masterPath of masters) {
123
+ const titleDir = dirname(masterPath);
124
+ const titleLabel = prettifyName(basename(titleDir));
125
+ let adoc;
126
+ try {
127
+ adoc = readFileSync(masterPath, "utf-8");
128
+ }
129
+ catch (err) {
130
+ const msg = err instanceof Error ? err.message : String(err);
131
+ throw new Error(`master-adoc loader: cannot read ${masterPath}: ${msg}`);
132
+ }
133
+ const includes = extractIncludes(adoc);
134
+ const navChildren = [];
135
+ for (const inc of includes) {
136
+ const resolvedPath = resolveIncludePath(masterPath, inc.path);
137
+ if (!existsSync(resolvedPath)) {
138
+ // Skip dangling includes; surface to the user via a warning
139
+ // rather than fail the whole load. The engine ALSO logs
140
+ // "Include file not found" so the user gets a second signal.
141
+ // eslint-disable-next-line no-console
142
+ console.warn(`master-adoc loader: include not found, skipping: ${inc.path} (from ${masterPath})`);
143
+ continue;
144
+ }
145
+ // Slug = resolved path relative to source root, without .adoc.
146
+ // Forward slashes for URL safety on Windows.
147
+ const slug = relative(sourceDir, resolvedPath)
148
+ .replace(/\\/g, "/")
149
+ .replace(/\.adoc$/, "");
150
+ if (!pageByPath.has(resolvedPath)) {
151
+ pageByPath.set(resolvedPath, {
152
+ filePath: resolvedPath,
153
+ slug,
154
+ ...(inc.leveloffset !== undefined ? { leveloffset: inc.leveloffset } : {}),
155
+ });
156
+ }
157
+ navChildren.push({
158
+ label: prettifyName(basename(slug)),
159
+ href: `${hrefPrefix}/${slug}`,
160
+ });
161
+ }
162
+ navGroups.push({ label: titleLabel, children: navChildren });
163
+ }
164
+ const pages = [...pageByPath.values()];
165
+ return { pages, nav: navGroups };
166
+ },
167
+ };
168
+ //# sourceMappingURL=master-adoc.js.map