@dogsbay/format-fluidtopics-rest 0.2.0-beta.101

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 +87 -0
  2. package/dist/assets.d.ts +13 -0
  3. package/dist/assets.d.ts.map +1 -0
  4. package/dist/assets.js +53 -0
  5. package/dist/assets.js.map +1 -0
  6. package/dist/cli.d.ts +9 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +28 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/frontmatter.d.ts +36 -0
  11. package/dist/frontmatter.d.ts.map +1 -0
  12. package/dist/frontmatter.js +78 -0
  13. package/dist/frontmatter.js.map +1 -0
  14. package/dist/headings.d.ts +13 -0
  15. package/dist/headings.d.ts.map +1 -0
  16. package/dist/headings.js +76 -0
  17. package/dist/headings.js.map +1 -0
  18. package/dist/html-to-tree.d.ts +17 -0
  19. package/dist/html-to-tree.d.ts.map +1 -0
  20. package/dist/html-to-tree.js +309 -0
  21. package/dist/html-to-tree.js.map +1 -0
  22. package/dist/importer.d.ts +87 -0
  23. package/dist/importer.d.ts.map +1 -0
  24. package/dist/importer.js +354 -0
  25. package/dist/importer.js.map +1 -0
  26. package/dist/index.d.ts +26 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +9 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/inline-html.d.ts +52 -0
  31. package/dist/inline-html.d.ts.map +1 -0
  32. package/dist/inline-html.js +157 -0
  33. package/dist/inline-html.js.map +1 -0
  34. package/dist/khub.d.ts +86 -0
  35. package/dist/khub.d.ts.map +1 -0
  36. package/dist/khub.js +111 -0
  37. package/dist/khub.js.map +1 -0
  38. package/dist/links.d.ts +13 -0
  39. package/dist/links.d.ts.map +1 -0
  40. package/dist/links.js +55 -0
  41. package/dist/links.js.map +1 -0
  42. package/dist/nav.d.ts +7 -0
  43. package/dist/nav.d.ts.map +1 -0
  44. package/dist/nav.js +15 -0
  45. package/dist/nav.js.map +1 -0
  46. package/dist/single-page.d.ts +70 -0
  47. package/dist/single-page.d.ts.map +1 -0
  48. package/dist/single-page.js +77 -0
  49. package/dist/single-page.js.map +1 -0
  50. package/dist/tree.d.ts +2 -0
  51. package/dist/tree.d.ts.map +1 -0
  52. package/dist/tree.js +2 -0
  53. package/dist/tree.js.map +1 -0
  54. package/package.json +50 -0
package/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # @dogsbay/format-fluidtopics-rest
2
+
3
+ Importer for a **live Fluid Topics portal** via the public **khub REST API**
4
+ (Option B of the FT→Dogsbay migration). It reverse-engineers each topic's
5
+ **DITA-OT HTML5** — where the DITA element vocabulary survives in `class` names
6
+ (`conbody`/`taskbody`/`refbody`, `shortdesc`, `section`, CALS tables, `span.ph`,
7
+ …) — into Dogsbay's universal `TreeNode` model, producing `ExportPage[]` +
8
+ `NavItem[]`.
9
+
10
+ Works on any **public** FT portal. Gated portals (RBAC) throw `KhubAuthError`.
11
+
12
+ The sibling [`@dogsbay/format-fluidtopics-markdown`](../format-fluidtopics-markdown)
13
+ does the same job from a Git Markdown export (Option A).
14
+
15
+ ## Usage
16
+
17
+ ```ts
18
+ import { importFluidTopicsRest } from "@dogsbay/format-fluidtopics-rest";
19
+
20
+ const { pages, nav } = await importFluidTopicsRest({
21
+ portalBase: "https://www.servicenow.com/docs", // or .../bas, docs.fluidtopics.com
22
+ titleFilter: "Customer Relationship Management", // or maps: ["<mapId>"]
23
+ hrefPrefix: "/docs",
24
+ });
25
+ ```
26
+
27
+ Or the low-level converter / client:
28
+
29
+ ```ts
30
+ import { htmlToTree, KhubClient } from "@dogsbay/format-fluidtopics-rest";
31
+ const { tree, title, topicType } = htmlToTree(htmlString); // topicType: concept|task|reference|topic
32
+ ```
33
+
34
+ CLI: `dogsbay migrate-fluidtopics-rest <portal-url> [--map <id>] [--title-filter <s>] [--single-page] [-o <dir>]`.
35
+
36
+ ### Single-page (whole-book) mode
37
+
38
+ Pass `--single-page` / `singlePage: true` to roll each map into **one page** (the
39
+ whole book) instead of one page per topic — for readers who prefer a single long
40
+ page over "bitty" topics, and to match FT portals that serve a book as one page.
41
+ TOC nodes become nested headings (level = TOC depth), internal xrefs become
42
+ in-page anchors, and the page lands at the book's common path prefix
43
+ (e.g. `…/metasys-ui-and-jct-technical-bulletin/16.0`).
44
+
45
+ ## What it does
46
+
47
+ - **khub harvest** — `maps` → `maps/{id}/toc` → `maps/{id}/topics/{cid}/content`; path-scoped bases (`…/bas/api/khub`); polite throttling; 401/403 → `KhubAuthError`.
48
+ - **HTML5 → TreeNode** — topic type from the body class; DITA sections flattened to headings; CALS tables → structured `table` nodes; `span.ph`/`keyword`/`uicontrol` unwrapped, `codeph` → inline code; FT chrome (`zDocs*`, `ai-summary`, `sentence`, `css-…`, `toggle/summary/tooltip`) stripped.
49
+ - **Frontmatter** — `topic_type` → `type`, plus `lang`/`version` from the map metadata.
50
+ - **Nav** — from each map's TOC (the authoritative DITA map), with route slugs derived from `prettyUrl`.
51
+
52
+ ## Rendering fidelity
53
+
54
+ Tuned against the live FT portals; each item is regression-tested. Full story in
55
+ [`docs-dev/fluidtopics-rest-importer.md`](../../docs-dev/fluidtopics-rest-importer.md).
56
+
57
+ - **Internal xrefs** — FT renders them as hrefless `<span data-tocid data-mapid>`
58
+ (resolved by its client at runtime, zero `href`s in the static HTML). The
59
+ importer builds a `tocId → slug` index from the selected maps' TOCs and resolves
60
+ them to real routes, falling back to plain text (not a dead link) when out of set.
61
+ - **Figures / inline images** — block `<img>` survive; base64 `data:` images are
62
+ extracted to real `content/_assets/…` files (filename from FT metadata).
63
+ - **Lists & inline runs** — consecutive inline content is coalesced into one
64
+ paragraph, so xrefs inside list items resolve and prose doesn't fragment.
65
+ - **Tables** — CALS → structured tables; cells with block content (lists,
66
+ paragraphs) are preserved (HTML tables per the Dogsbay-MD convention).
67
+ - **Definition lists** — `<dl>/<dt>/<dd>` → a real deflist (`dt.inline` term + `dd`
68
+ block children), not literal `: ` text.
69
+
70
+ ## Verified (live, 2026-06)
71
+
72
+ ServiceNow `www.servicenow.com/docs` (CRM, 24 topics), Johnson Controls
73
+ `docs.johnsoncontrols.com/bas` (Metasys bulletin, 55), Fluid Topics
74
+ `docs.fluidtopics.com` (13). Notably, REST succeeds on ServiceNow's CRM
75
+ publication where the **Markdown export (Option A) failed** (its `index.md` TOC
76
+ was stale: 1/24 targets existed) — the live khub TOC+content is authoritative.
77
+
78
+ ## Known gaps
79
+
80
+ - Nav-card landing grids come through as a generic table (Option A special-cases
81
+ them into `:::cards`) — a `nav-card`-detection pass is future work.
82
+ - **Cross-map** xrefs resolve only when the target map is in the import set
83
+ (single-`--map` runs leave them as text).
84
+ - External image **URLs** are kept as-is (not downloaded); only inline `data:`
85
+ images are localized to `_assets`.
86
+ - conref/keyref/profiling were already resolved/flattened upstream by FT's
87
+ DITA-OT run (not recoverable, and not needed for delivery).
@@ -0,0 +1,13 @@
1
+ export interface ParsedDataUri {
2
+ mime: string;
3
+ data: Buffer;
4
+ }
5
+ /** Parse a `data:` URI into its mime + decoded bytes. Returns null if not one. */
6
+ export declare function parseDataUri(src: string): ParsedDataUri | null;
7
+ export declare function sanitizeFilename(name: string): string;
8
+ /**
9
+ * Compute the content-relative asset path (`_assets/<dir>/<file>`) for a
10
+ * decoded image, using the FT display-name as the filename.
11
+ */
12
+ export declare function assetPath(resourceId: string | undefined, displayName: string | undefined, parsed: ParsedDataUri): string;
13
+ //# sourceMappingURL=assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAQ9D;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQrD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,MAAM,EAAE,aAAa,GACpB,MAAM,CAOR"}
package/dist/assets.js ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Turn inline `data:` images (FT inlines figure PNGs / SVG icons as base64) into
3
+ * real content-rooted asset files, per the Dogsbay `_assets` convention:
4
+ * bytes written under `content/_assets/<rel>/<file>` and referenced as
5
+ * `/_assets/<rel>/<file>` (format-astro's copyAssets propagates them to
6
+ * astro/public on build).
7
+ *
8
+ * The human filename comes from FT metadata (`data-ft-asset-display-name`); the
9
+ * directory is an 8-char hash of the FT `data-ft-resource-id` (stable + dedups
10
+ * the same resource reused across topics) or, lacking that, of the bytes.
11
+ */
12
+ import { createHash } from "node:crypto";
13
+ const EXT_BY_MIME = {
14
+ "image/png": "png",
15
+ "image/jpeg": "jpg",
16
+ "image/jpg": "jpg",
17
+ "image/gif": "gif",
18
+ "image/svg+xml": "svg",
19
+ "image/webp": "webp",
20
+ "image/avif": "avif",
21
+ };
22
+ /** Parse a `data:` URI into its mime + decoded bytes. Returns null if not one. */
23
+ export function parseDataUri(src) {
24
+ const m = /^data:([^;,]+)(;base64)?,([\s\S]*)$/.exec(src);
25
+ if (!m)
26
+ return null;
27
+ const mime = m[1];
28
+ const isBase64 = !!m[2];
29
+ const payload = m[3];
30
+ const data = isBase64 ? Buffer.from(payload, "base64") : Buffer.from(decodeURIComponent(payload), "utf-8");
31
+ return { mime, data };
32
+ }
33
+ export function sanitizeFilename(name) {
34
+ return (name
35
+ .trim()
36
+ .replace(/[^\w.\-]+/g, "-")
37
+ .replace(/-+/g, "-")
38
+ .replace(/^[-.]+|[-.]+$/g, "") || "image");
39
+ }
40
+ /**
41
+ * Compute the content-relative asset path (`_assets/<dir>/<file>`) for a
42
+ * decoded image, using the FT display-name as the filename.
43
+ */
44
+ export function assetPath(resourceId, displayName, parsed) {
45
+ const ext = EXT_BY_MIME[parsed.mime] ?? "bin";
46
+ let file = displayName ? sanitizeFilename(displayName) : "";
47
+ if (!file || !/\.[a-z0-9]+$/i.test(file))
48
+ file = `${file || "image"}.${ext}`;
49
+ const key = resourceId || createHash("sha1").update(parsed.data).digest("hex");
50
+ const dir = createHash("sha1").update(key).digest("hex").slice(0, 8);
51
+ return `_assets/${dir}/${file}`;
52
+ }
53
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.js","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,WAAW,GAA2B;IAC1C,WAAW,EAAE,KAAK;IAClB,YAAY,EAAE,KAAK;IACnB,WAAW,EAAE,KAAK;IAClB,WAAW,EAAE,KAAK;IAClB,eAAe,EAAE,KAAK;IACtB,YAAY,EAAE,MAAM;IACpB,YAAY,EAAE,MAAM;CACrB,CAAC;AAOF,kFAAkF;AAClF,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,CAAC,GAAG,qCAAqC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1D,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClB,MAAM,QAAQ,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxB,MAAM,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACrB,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;IAC3G,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,IAAY;IAC3C,OAAO,CACL,IAAI;SACD,IAAI,EAAE;SACN,OAAO,CAAC,YAAY,EAAE,GAAG,CAAC;SAC1B,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;SACnB,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,IAAI,OAAO,CAC5C,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CACvB,UAA8B,EAC9B,WAA+B,EAC/B,MAAqB;IAErB,MAAM,GAAG,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC;IAC9C,IAAI,IAAI,GAAG,WAAW,CAAC,CAAC,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC5D,IAAI,CAAC,IAAI,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,IAAI,GAAG,GAAG,IAAI,IAAI,OAAO,IAAI,GAAG,EAAE,CAAC;IAC7E,MAAM,GAAG,GAAG,UAAU,IAAI,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC/E,MAAM,GAAG,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACrE,OAAO,WAAW,GAAG,IAAI,IAAI,EAAE,CAAC;AAClC,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { FormatPlugin } from "@dogsbay/types";
2
+ /**
3
+ * Fluid Topics REST format plugin.
4
+ *
5
+ * Import-only; the "source" is a **portal URL** (not a local path), so it's
6
+ * driven by the `migrate-fluidtopics-rest` command rather than path auto-detect.
7
+ */
8
+ export declare const plugin: FormatPlugin;
9
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAuB,MAAM,gBAAgB,CAAC;AAGxE;;;;;GAKG;AACH,eAAO,MAAM,MAAM,EAAE,YAoBpB,CAAC"}
package/dist/cli.js ADDED
@@ -0,0 +1,28 @@
1
+ import { importFluidTopicsRest } from "./importer.js";
2
+ /**
3
+ * Fluid Topics REST format plugin.
4
+ *
5
+ * Import-only; the "source" is a **portal URL** (not a local path), so it's
6
+ * driven by the `migrate-fluidtopics-rest` command rather than path auto-detect.
7
+ */
8
+ export const plugin = {
9
+ name: "fluidtopics-rest",
10
+ canImport: true,
11
+ canExport: false,
12
+ detectSource: () => false, // URL source — invoked explicitly
13
+ importOptions: [],
14
+ async import(source, opts) {
15
+ const { pages, nav } = await importFluidTopicsRest({
16
+ portalBase: source,
17
+ maps: Array.isArray(opts.maps) ? opts.maps : undefined,
18
+ titleFilter: typeof opts.titleFilter === "string" ? opts.titleFilter : undefined,
19
+ lang: typeof opts.lang === "string" ? opts.lang : undefined,
20
+ hrefPrefix: typeof opts.hrefPrefix === "string" ? opts.hrefPrefix : undefined,
21
+ throttleMs: typeof opts.throttleMs === "number" ? opts.throttleMs : undefined,
22
+ taxonomyNames: Array.isArray(opts.taxonomyNames) ? opts.taxonomyNames : undefined,
23
+ });
24
+ opts.siteName = opts.siteName || "Documentation";
25
+ return { pages, nav };
26
+ },
27
+ };
28
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAEtD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,MAAM,GAAiB;IAClC,IAAI,EAAE,kBAAkB;IACxB,SAAS,EAAE,IAAI;IACf,SAAS,EAAE,KAAK;IAChB,YAAY,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,kCAAkC;IAC7D,aAAa,EAAE,EAAE;IAEjB,KAAK,CAAC,MAAM,CAAC,MAAc,EAAE,IAA6B;QACxD,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,MAAM,qBAAqB,CAAC;YACjD,UAAU,EAAE,MAAM;YAClB,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,IAAiB,CAAC,CAAC,CAAC,SAAS;YACpE,WAAW,EAAE,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;YAChF,IAAI,EAAE,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;YAC3D,UAAU,EAAE,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;YAC7E,UAAU,EAAE,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;YAC7E,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,aAA0B,CAAC,CAAC,CAAC,SAAS;SAChG,CAAC,CAAC;QACH,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,eAAe,CAAC;QACjD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;IACxB,CAAC;CACF,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Build Dogsbay frontmatter for a topic from the DITA-OT-derived topic type
3
+ * plus the map's khub metadata (locale, release/family).
4
+ */
5
+ import type { KhubMap } from "./khub.js";
6
+ type Meta = NonNullable<KhubMap["metadata"]>;
7
+ export interface BuildFrontmatterArgs {
8
+ title: string;
9
+ /** concept | task | reference | glossentry | topic */
10
+ topicType: string;
11
+ mapMeta?: Meta;
12
+ /**
13
+ * Per-topic metadata (from `/maps/{id}/topics`). When present its values take
14
+ * precedence over `mapMeta` — topic-level facets are more specific than the
15
+ * map's. The DITA subjectScheme classification arrives here as the `PBS`
16
+ * ("Subject matter") facet.
17
+ */
18
+ topicMeta?: Meta;
19
+ /**
20
+ * Extra FT metadata keys to copy into frontmatter, kept **verbatim** (the
21
+ * `ft:` prefix is preserved to namespace them). Allowlist, not blocklist: the
22
+ * internal `ft:` keys never land unless explicitly named here.
23
+ */
24
+ metadataAllow?: readonly string[];
25
+ /**
26
+ * Emit the `ft:`-namespaced technical numerics (wordCount/contentSize/…).
27
+ * True for book/single-page pages (the map totals are correct there) and for
28
+ * per-topic pages only when full per-topic metadata was fetched. Off on the
29
+ * cheap per-topic path, where the only available values are misleading book
30
+ * totals.
31
+ */
32
+ technical?: boolean;
33
+ }
34
+ export declare function buildFrontmatter(args: BuildFrontmatterArgs): Record<string, unknown>;
35
+ export {};
36
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,KAAK,IAAI,GAAG,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;AAmB7C,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,IAAI,CAAC;IACf;;;;;OAKG;IACH,SAAS,CAAC,EAAE,IAAI,CAAC;IACjB;;;;OAIG;IACH,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAkDpF"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * FT technical metadata kept verbatim under the `ft:` namespace (as numbers) so
3
+ * they sit apart from the semantic facets. `wordCount`/`outputSize` are
4
+ * per-topic; `contentSize`/`attachmentsSize` are map-level (so they repeat
5
+ * across a book's topics).
6
+ */
7
+ const TECHNICAL_KEYS = ["ft:wordCount", "ft:contentSize", "ft:outputSize", "ft:attachmentsSize"];
8
+ function metaValue(meta, ...keys) {
9
+ if (!meta)
10
+ return undefined;
11
+ for (const key of keys) {
12
+ const hit = meta.find((m) => m.key === key);
13
+ if (hit?.values?.length)
14
+ return hit.values[0];
15
+ }
16
+ return undefined;
17
+ }
18
+ export function buildFrontmatter(args) {
19
+ const fm = { title: args.title };
20
+ // Prefer the per-topic value, fall back to the map's.
21
+ const mv = (...keys) => metaValue(args.topicMeta, ...keys) ?? metaValue(args.mapMeta, ...keys);
22
+ // topic_type → type (skip the generic "topic")
23
+ if (args.topicType && args.topicType !== "topic")
24
+ fm.type = args.topicType;
25
+ const locale = mv("ft:locale", "locale");
26
+ if (locale)
27
+ fm.lang = locale;
28
+ const version = mv("family", "version", "ft:version", "Version_FT", "FT_Version");
29
+ if (version)
30
+ fm.version = version;
31
+ // Faceted metadata → search filters (Pagefind auto-discovers the
32
+ // `data-pagefind-filter` divs DocsLayout emits from these fields).
33
+ // `category` / `audience` are built-in facets (parseMeta reads them
34
+ // directly); `editorialType` / `version` / `subject` are custom taxonomies
35
+ // the importer's `taxonomyNames` picks up. See docs-dev/fluidtopics-rest-importer.md.
36
+ const category = mv("Category");
37
+ if (category)
38
+ fm.category = category;
39
+ const audience = mv("audience");
40
+ if (audience)
41
+ fm.audience = audience;
42
+ const editorialType = mv("ft:editorialType");
43
+ if (editorialType)
44
+ fm.editorialType = editorialType;
45
+ // DITA subjectScheme classification → "Subject matter" facet.
46
+ const subject = mv("PBS");
47
+ if (subject)
48
+ fm.subject = subject;
49
+ const product = mv("Product");
50
+ if (product)
51
+ fm.product = product;
52
+ const author = mv("author");
53
+ if (author)
54
+ fm.author = author;
55
+ const source = mv("ft:sourceName");
56
+ if (source)
57
+ fm.source = source;
58
+ // Last content change → DocsFooter "Last updated". Per-topic only: it lives
59
+ // solely in the full per-topic metadata, so it's absent on the cheap path
60
+ // (we deliberately don't fall back to the book-level ft:lastEdition).
61
+ const updated = metaValue(args.topicMeta, "ft:lastTechChange");
62
+ if (updated)
63
+ fm.updated = updated;
64
+ // Technical fields (only where the values are meaningful — see `technical`),
65
+ // plus any --metadata extras (always): kept under the `ft:` namespace,
66
+ // verbatim, coerced to number when numeric.
67
+ const keys = [...(args.technical ? TECHNICAL_KEYS : []), ...(args.metadataAllow ?? [])];
68
+ for (const key of keys) {
69
+ if (key in fm)
70
+ continue;
71
+ const v = mv(key);
72
+ if (v === undefined)
73
+ continue;
74
+ fm[key] = /^\d+$/.test(v) ? Number(v) : v;
75
+ }
76
+ return fm;
77
+ }
78
+ //# sourceMappingURL=frontmatter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.js","sourceRoot":"","sources":["../src/frontmatter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Heading extraction + slug population.
3
+ *
4
+ * Walks the tree, computes a stable slug for each heading (de-duplicated),
5
+ * writes `slug`/`text` back onto the heading node's props (so the renderer
6
+ * can emit anchors), and returns the flat Heading[] for TOC generation.
7
+ */
8
+ import type { TreeNode, Heading } from "@dogsbay/types";
9
+ export declare function extractHeadings(tree: TreeNode[]): Heading[];
10
+ /** Extract plain text from a node, preferring structured inline content. */
11
+ export declare function extractText(node: TreeNode): string;
12
+ export declare function slugify(text: string): string;
13
+ //# sourceMappingURL=headings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headings.d.ts","sourceRoot":"","sources":["../src/headings.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAc,MAAM,gBAAgB,CAAC;AAEpE,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,OAAO,EAAE,CA6B3D;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAWlD;AAyBD,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAU5C"}
@@ -0,0 +1,76 @@
1
+ export function extractHeadings(tree) {
2
+ const headings = [];
3
+ const seen = new Map();
4
+ function walk(nodes) {
5
+ for (const node of nodes) {
6
+ if (node.type === "heading") {
7
+ const level = node.props?.level ?? 1;
8
+ const text = extractText(node);
9
+ let slug = node.props?.slug;
10
+ if (!slug) {
11
+ const base = slugify(text);
12
+ const count = seen.get(base) ?? 0;
13
+ slug = count === 0 ? base : `${base}-${count}`;
14
+ seen.set(base, count + 1);
15
+ }
16
+ if (!node.props)
17
+ node.props = {};
18
+ node.props.slug = slug;
19
+ node.props.text = text;
20
+ headings.push({ depth: level, slug, text });
21
+ }
22
+ if (node.children)
23
+ walk(node.children);
24
+ }
25
+ }
26
+ walk(tree);
27
+ return headings;
28
+ }
29
+ /** Extract plain text from a node, preferring structured inline content. */
30
+ export function extractText(node) {
31
+ const parts = [];
32
+ if (node.inline) {
33
+ parts.push(extractInlineText(node.inline));
34
+ }
35
+ else if (node.html) {
36
+ parts.push(node.html.replace(/<[^>]+>/g, ""));
37
+ }
38
+ if (node.children) {
39
+ for (const child of node.children)
40
+ parts.push(extractText(child));
41
+ }
42
+ return parts.filter(Boolean).join(" ").trim();
43
+ }
44
+ function extractInlineText(nodes) {
45
+ const parts = [];
46
+ for (const node of nodes) {
47
+ switch (node.type) {
48
+ case "text":
49
+ case "code":
50
+ parts.push(node.text);
51
+ break;
52
+ case "link":
53
+ case "highlight":
54
+ parts.push(extractInlineText(node.children));
55
+ break;
56
+ case "image":
57
+ if (node.alt)
58
+ parts.push(node.alt);
59
+ break;
60
+ case "html-inline":
61
+ parts.push(node.html.replace(/<[^>]+>/g, ""));
62
+ break;
63
+ }
64
+ }
65
+ return parts.join("");
66
+ }
67
+ export function slugify(text) {
68
+ return (text
69
+ .toLowerCase()
70
+ .replace(/[^\w\s-]/g, "")
71
+ .replace(/\s+/g, "-")
72
+ .replace(/-+/g, "-")
73
+ .replace(/^-+|-+$/g, "")
74
+ .trim() || "section");
75
+ }
76
+ //# sourceMappingURL=headings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headings.js","sourceRoot":"","sources":["../src/headings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { TreeNode } from "@dogsbay/types";
2
+ import { type InlineOptions } from "./inline-html.js";
3
+ export interface HtmlToTreeResult {
4
+ tree: TreeNode[];
5
+ /** Topic title from h1.topictitle1, if present. */
6
+ title?: string;
7
+ /** Derived from the body class: concept | task | reference | topic. */
8
+ topicType: string;
9
+ }
10
+ export declare function htmlToTree(html: string, opts?: InlineOptions): HtmlToTreeResult;
11
+ /**
12
+ * Extract a YouTube video id from an embed/watch/short URL, else undefined.
13
+ * Handles youtube.com/embed/<id>, youtube-nocookie.com/embed/<id>,
14
+ * youtu.be/<id>, and watch?v=<id>.
15
+ */
16
+ export declare function youtubeIdFromUrl(url: string): string | undefined;
17
+ //# sourceMappingURL=html-to-tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html-to-tree.d.ts","sourceRoot":"","sources":["../src/html-to-tree.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAiD,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAErG,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,QAAQ,EAAE,CAAC;IACjB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;CACnB;AAWD,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,aAAkB,GAAG,gBAAgB,CAYnF;AAyBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKhE"}