@dogsbay/adapter-tigera 0.0.0-stage → 0.2.0-beta.135

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.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Where a Tigera `_includes/` partial import points.
3
+ *
4
+ * Tigera authors share content via Docusaurus MDX imports:
5
+ *
6
+ * import Selectors from '@site/calico/_includes/content/_selectors.mdx';
7
+ * ...
8
+ * <Selectors />
9
+ *
10
+ * Spliced in place by the importer's own splicer (`splicePartials`, called from the adapter's
11
+ * preprocess so `$[variable]` substitution reaches the partials' text), verbatim and with NO
12
+ * heading-level offset: Docusaurus renders a partial's headings at their literal levels.
13
+ * `--partial-mode include` writes them as includes instead (structural.ts), with this resolver.
14
+ */
15
+ import { type PreprocessContext } from "@dogsbay/format-docusaurus/adapter";
16
+ /**
17
+ * Resolve an import specifier (`@site/` or relative) to the partial it names, through the corpus:
18
+ * a corpus path inside the project, an absolute path in an `--include-root` folder (CLI), or null.
19
+ * The corpus is the containment: `@site/../x`, `../../x` or a symlink out of the project resolve
20
+ * to nothing.
21
+ */
22
+ export declare function resolveSpec(spec: string, ctx: PreprocessContext): string | null;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Where a Tigera `_includes/` partial import points.
3
+ *
4
+ * Tigera authors share content via Docusaurus MDX imports:
5
+ *
6
+ * import Selectors from '@site/calico/_includes/content/_selectors.mdx';
7
+ * ...
8
+ * <Selectors />
9
+ *
10
+ * Spliced in place by the importer's own splicer (`splicePartials`, called from the adapter's
11
+ * preprocess so `$[variable]` substitution reaches the partials' text), verbatim and with NO
12
+ * heading-level offset: Docusaurus renders a partial's headings at their literal levels.
13
+ * `--partial-mode include` writes them as includes instead (structural.ts), with this resolver.
14
+ */
15
+ import { partialFiles } from "@dogsbay/format-docusaurus/adapter";
16
+ /**
17
+ * Resolve an import specifier (`@site/` or relative) to the partial it names, through the corpus:
18
+ * a corpus path inside the project, an absolute path in an `--include-root` folder (CLI), or null.
19
+ * The corpus is the containment: `@site/../x`, `../../x` or a symlink out of the project resolve
20
+ * to nothing.
21
+ */
22
+ export function resolveSpec(spec, ctx) {
23
+ if (ctx.file === undefined)
24
+ return null;
25
+ if (!spec.startsWith("@site/") && !spec.startsWith("."))
26
+ return null; // a package
27
+ const relSpec = spec.startsWith(".") && !spec.startsWith("./") && !spec.startsWith("../") ? `./${spec}` : spec;
28
+ return partialFiles(ctx).resolve(relSpec, ctx.file) ?? null;
29
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @dogsbay/adapter-tigera
3
+ *
4
+ * Site adapter for the Tigera/Calico docs (docs.tigera.io). Injected into the
5
+ * generic @dogsbay/format-docusaurus importer via `--adapter tigera`. Holds
6
+ * everything Tigera layered on top of standard Docusaurus:
7
+ * - `$[variable]` substitution (custom remark plugin → adapter preprocess)
8
+ * - Tigera-specific MDX components (DocCardLink, GeekDetails, If, ...)
9
+ *
10
+ * The generic importer contains ZERO Tigera code; all of it lives here.
11
+ */
12
+ import { type DocusaurusAdapter } from "@dogsbay/format-docusaurus/adapter";
13
+ export declare const tigeraAdapter: DocusaurusAdapter;
14
+ export { loadVariables, substituteVariables } from "./variables.js";
15
+ /**
16
+ * Is this the Tigera docs repo (docs.tigera.io)? From its paths, relative to the site's folder
17
+ * (a detection's `root`, as `migrate` takes them): Tigera's own `$[variable]` remark plugin, and a `variables.js` in a docs folder. For
18
+ * a host offering the adapter, never applying it silently.
19
+ */
20
+ export declare function detect(paths: Iterable<string>): boolean;
package/dist/index.js ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @dogsbay/adapter-tigera
3
+ *
4
+ * Site adapter for the Tigera/Calico docs (docs.tigera.io). Injected into the
5
+ * generic @dogsbay/format-docusaurus importer via `--adapter tigera`. Holds
6
+ * everything Tigera layered on top of standard Docusaurus:
7
+ * - `$[variable]` substitution (custom remark plugin → adapter preprocess)
8
+ * - Tigera-specific MDX components (DocCardLink, GeekDetails, If, ...)
9
+ *
10
+ * The generic importer contains ZERO Tigera code; all of it lives here.
11
+ */
12
+ import { splicePartials } from "@dogsbay/format-docusaurus/adapter";
13
+ import { loadVariables, substituteVariables } from "./variables.js";
14
+ import { toStructural } from "./structural.js";
15
+ // ── Tigera custom components ────────────────────────────
16
+ //
17
+ // Mappings keep output clean instead of leaking raw JSX. Card-like components
18
+ // map to Dogsbay cards; conditional/wrapper components keep their inner content;
19
+ // data-driven components with no static content are stripped (self-closing).
20
+ const TIGERA_COMPONENTS = {
21
+ // Cross-page link cards (from /src/___new___/components). They reference a
22
+ // target via `docId` (Docusaurus doc id); the card title comes from that
23
+ // doc's frontmatter. Emit `docId` for the generic parser to resolve into
24
+ // title + href (an explicit title/href attr still wins if present).
25
+ DocCardLink: {
26
+ type: "card",
27
+ container: false,
28
+ propsFromAttrs: (a) => ({ docId: a.docId, title: a.title || a.label, href: a.to || a.href }),
29
+ },
30
+ PaidProductDocCardLink: {
31
+ type: "card",
32
+ container: false,
33
+ propsFromAttrs: (a) => ({ docId: a.docId, title: a.title || a.label, href: a.to || a.href }),
34
+ },
35
+ DocCardLinkLayout: { type: "cards", container: true },
36
+ CalicoProducts: { type: "cards", container: true },
37
+ // Conditional rendering — keep the inner content, drop the wrapper.
38
+ If: { type: "if", container: true, strip: true },
39
+ // Metadata/badge components with no reproducible static content.
40
+ GeekDetails: { type: "badge", container: false, strip: true },
41
+ // NOTE: `<Screenshot>` (a plain <img> wrapper) is handled by an inline
42
+ // transform → markdown image, NOT a strip mapping — see TIGERA_INLINE_TRANSFORMS.
43
+ };
44
+ // `_includes/components/*.js` — data-driven React components (render tables from
45
+ // JSON, generate env files, etc.). They are CODE and can't be inlined like the
46
+ // .mdx content partials. Map them to a placeholder (not a silent strip) so they
47
+ // don't leak as raw `<Tag/>` text AND don't leave a container empty — a stripped
48
+ // component that was the sole content of a tab made the tab label vanish.
49
+ const TIGERA_JS_COMPONENTS = [
50
+ "AutoHostendpointsMigrate",
51
+ "CalicoWindowsInstall",
52
+ "ComponentVersions",
53
+ "ConfigDataplaneOpenShiftManifests",
54
+ "DownloadOpenShiftManifests",
55
+ "EnvironmentFile",
56
+ "HostEndpointsUpgrade",
57
+ "InstallOpenShiftManifests",
58
+ "MaintenanceImageOptionsAlternateRegistry",
59
+ "FelixConfig",
60
+ ];
61
+ for (const name of TIGERA_JS_COMPONENTS) {
62
+ TIGERA_COMPONENTS[name] = { type: "__placeholder", container: true };
63
+ }
64
+ // Self-closing Tigera tags (no closing tag expected).
65
+ const TIGERA_SELF_CLOSING = [
66
+ "DocCardLink",
67
+ "PaidProductDocCardLink",
68
+ "GeekDetails",
69
+ "CalicoProducts",
70
+ ...TIGERA_JS_COMPONENTS,
71
+ ];
72
+ // Inline replacements for icon components used within prose.
73
+ const TIGERA_INLINE_TRANSFORMS = [
74
+ // Chakra checkmark icon → unicode check.
75
+ [/<CheckIcon\b[^>]*\/>/g, "✓"],
76
+ // @docusaurus/Link (`<Link to=…>` or `<Link href=…>`) → a raw <a> so it works
77
+ // both in prose AND inside raw-HTML tables (a markdown link wouldn't render
78
+ // inside an HTML `<td>`). Reads either the `to` or `href` attribute.
79
+ [
80
+ /<Link\b([^>]*?)>([\s\S]*?)<\/Link>/g,
81
+ (_m, attrs, text) => {
82
+ const url = attrs.match(/\b(?:to|href)=["']([^"']+)["']/)?.[1];
83
+ const inner = text.trim();
84
+ return url ? `<a href="${url}">${inner}</a>` : inner;
85
+ },
86
+ ],
87
+ // <Screenshot src alt /> is a plain <img> wrapper → markdown image (the src is
88
+ // then `/_assets`-rewritten by the generic parser, and the static file copied).
89
+ [
90
+ /<Screenshot\b([^>]*?)\/>/g,
91
+ (_m, attrs) => {
92
+ const src = attrs.match(/\bsrc=["']([^"']*)["']/)?.[1] ?? "";
93
+ const alt = attrs.match(/\balt=["']([^"']*)["']/)?.[1] ?? "";
94
+ return src ? `![${alt}](${src})` : "";
95
+ },
96
+ ],
97
+ ];
98
+ export const tigeraAdapter = {
99
+ name: "tigera",
100
+ splicesPartials: true,
101
+ preprocess(source, ctx) {
102
+ // Tigera numbers figures with an MDX counter (`export let figCount = 1` +
103
+ // `{figCount++}` in captions). The export is stripped and the expression
104
+ // can't be evaluated, so substitute sequential numbers per page.
105
+ let fig = 0;
106
+ let text = source.replace(/\{\s*figCount\+\+\s*\}/g, () => String(++fig));
107
+ // MDX whitespace expressions `{' '}` / `{" "}` (used to force a space, e.g.
108
+ // inside table cells) → a literal space.
109
+ text = text.replace(/\{\s*(['"])(\s*)\1\s*\}/g, "$2");
110
+ // Partials: either splice bodies in place (inline, default; the importer's splicer, here so
111
+ // the variables below reach them) or emit {% include %}/{% set %}/{% if %} directives +
112
+ // collect fragments (include).
113
+ const partials = ctx.partialMode === "include"
114
+ ? // .md partials, which include mode does not write as includes, are spliced
115
+ splicePartials(toStructural(text, ctx), ctx)
116
+ : splicePartials(text, ctx);
117
+ // Substitute $[variables] (Phase 1 keeps this literal in both modes).
118
+ if (ctx.instanceRoot === undefined)
119
+ return partials;
120
+ return substituteVariables(partials, loadVariables(ctx));
121
+ },
122
+ components: TIGERA_COMPONENTS,
123
+ selfClosingTags: TIGERA_SELF_CLOSING,
124
+ inlineTransforms: TIGERA_INLINE_TRANSFORMS,
125
+ };
126
+ export { loadVariables, substituteVariables } from "./variables.js";
127
+ /**
128
+ * Is this the Tigera docs repo (docs.tigera.io)? From its paths, relative to the site's folder
129
+ * (a detection's `root`, as `migrate` takes them): Tigera's own `$[variable]` remark plugin, and a `variables.js` in a docs folder. For
130
+ * a host offering the adapter, never applying it silently.
131
+ */
132
+ export function detect(paths) {
133
+ let plugin = false;
134
+ let variables = false;
135
+ for (const p of paths) {
136
+ if (/^src\/remark\/variablesPlugin\.[cm]?[jt]s$/.test(p))
137
+ plugin = true;
138
+ else if (/^[^/]+(?:_versioned_docs\/[^/]+)?\/variables\.js$/.test(p))
139
+ variables = true;
140
+ if (plugin && variables)
141
+ return true;
142
+ }
143
+ return false;
144
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Structural partial mode (mirrors AsciiDoc's `inlineIncludes: false`).
3
+ *
4
+ * Instead of inlining `_includes/` partials, emit Minja directives as text and
5
+ * collect each referenced `.mdx` partial as a *fragment* to be written at its
6
+ * include path. The build-time Minja preprocessor resolves them later.
7
+ *
8
+ * Transforms (text → text, before parsing):
9
+ * - `export const X = '…'` → `{% set X = "…" %}`
10
+ * - `<Partial a="1" b={x} />` (.mdx import)→ `{% set a = "1" %}` (only when a≠source)
11
+ * `{% include "…/_partial.md" %}`
12
+ * - `<If condition={props.x === 'y'}>…</If>`→ `{% if x == "y" %}…{% endif %}`
13
+ *
14
+ * `$[var]` substitution still runs afterwards (Phase 1; structural variables are
15
+ * Phase 2). `.js` component partials are left for the component-map strip.
16
+ */
17
+ import { type PreprocessContext } from "@dogsbay/format-docusaurus/adapter";
18
+ /**
19
+ * Translate a Docusaurus `<If condition={…}>` JSX expression into a Minja
20
+ * expression. Supports the subset that actually appears in Tigera:
21
+ * props.x, ===, !==, &&, ||, !, parentheses, string/number/bool literals.
22
+ * Returns null when the expression is outside this grammar (caller falls back
23
+ * to inline + warn).
24
+ */
25
+ export declare function jsxConditionToMinja(expr: string): string | null;
26
+ /**
27
+ * Map a partial (a corpus path, or an absolute path in an `--include-root` folder) to its
28
+ * include path, relative to the instance folder. A partial elsewhere in the project is stored
29
+ * under `_project/`, one from an `--include-root` folder under `_include-roots/<folder name>/`
30
+ * (the CLI places it, by real path): relative to the instance either would climb out of the
31
+ * content root and be written outside the output. Undefined when the partial is in none of
32
+ * them (it is then not collected).
33
+ */
34
+ export declare function fragmentIncludePath(path: string, ctx: Pick<PreprocessContext, "instanceRoot" | "projectRoot" | "external">): string | undefined;
35
+ /**
36
+ * Translate `<If condition={…}>…</If>` blocks to `{% if … %}…{% endif %}`.
37
+ * Returns the rewritten source plus a flag set when any block could not be
38
+ * translated (caller can decide to fall back to inline for that page/partial).
39
+ */
40
+ export declare function rewriteIfBlocks(source: string): {
41
+ text: string;
42
+ untranslated: number;
43
+ };
44
+ /**
45
+ * Apply the structural transform: emit Minja directives + collect fragments.
46
+ * Used for both pages and (recursively, via the importer draining
47
+ * `ctx.fragments`) the partial fragments themselves.
48
+ */
49
+ export declare function toStructural(source: string, ctx: PreprocessContext): string;
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Structural partial mode (mirrors AsciiDoc's `inlineIncludes: false`).
3
+ *
4
+ * Instead of inlining `_includes/` partials, emit Minja directives as text and
5
+ * collect each referenced `.mdx` partial as a *fragment* to be written at its
6
+ * include path. The build-time Minja preprocessor resolves them later.
7
+ *
8
+ * Transforms (text → text, before parsing):
9
+ * - `export const X = '…'` → `{% set X = "…" %}`
10
+ * - `<Partial a="1" b={x} />` (.mdx import)→ `{% set a = "1" %}` (only when a≠source)
11
+ * `{% include "…/_partial.md" %}`
12
+ * - `<If condition={props.x === 'y'}>…</If>`→ `{% if x == "y" %}…{% endif %}`
13
+ *
14
+ * `$[var]` substitution still runs afterwards (Phase 1; structural variables are
15
+ * Phase 2). `.js` component partials are left for the component-map strip.
16
+ */
17
+ import { posix, relativeDir } from "@dogsbay/format-docusaurus/adapter";
18
+ import { resolveSpec } from "./includes.js";
19
+ // ── JSX condition → Minja expression ────────────────────
20
+ /**
21
+ * Translate a Docusaurus `<If condition={…}>` JSX expression into a Minja
22
+ * expression. Supports the subset that actually appears in Tigera:
23
+ * props.x, ===, !==, &&, ||, !, parentheses, string/number/bool literals.
24
+ * Returns null when the expression is outside this grammar (caller falls back
25
+ * to inline + warn).
26
+ */
27
+ export function jsxConditionToMinja(expr) {
28
+ let s = expr.trim();
29
+ if (!s)
30
+ return null;
31
+ // Reject constructs we don't translate (calls, member access beyond props.x,
32
+ // ternaries, template literals, arithmetic).
33
+ if (/`|\?|=>|\.\s*\w+\s*\(|\bfunction\b/.test(s))
34
+ return null;
35
+ // props.x → x (also bare `props` is invalid on its own)
36
+ s = s.replace(/\bprops\.([A-Za-z_$][\w$]*)/g, "$1");
37
+ if (/\bprops\b/.test(s))
38
+ return null;
39
+ // Operators: order matters (=== before ==, !== before !).
40
+ s = s.replace(/===/g, "==").replace(/!==/g, "!=");
41
+ s = s.replace(/&&/g, " and ").replace(/\|\|/g, " or ");
42
+ // Unary not: `!x` / `!(…)` → `not x`. Leave `!=` intact (already handled).
43
+ s = s.replace(/!(?!=)\s*/g, "not ");
44
+ // Normalize double/single quotes to double for Minja string literals.
45
+ s = s.replace(/'([^']*)'/g, '"$1"');
46
+ // Whitelist final character set — identifiers, comparison, logic, literals.
47
+ // (Defensive: anything exotic that slipped through → bail.)
48
+ if (/[^\w\s."'=!<>()and ortnu]/.test(s.replace(/"[^"]*"/g, '""'))) {
49
+ // Allow only a safe charset outside string literals.
50
+ // (The regex above is intentionally loose; rely on the rejects above too.)
51
+ }
52
+ return s.replace(/\s+/g, " ").trim();
53
+ }
54
+ function parseMdxImports(source, ctx) {
55
+ const out = [];
56
+ const re = /^\s*import\s+([A-Z][A-Za-z0-9_]*)\s+from\s+['"]([^'"]+)['"]\s*;?\s*$/gm;
57
+ let m;
58
+ while ((m = re.exec(source)) !== null) {
59
+ const [, name, spec] = m;
60
+ if (!spec.endsWith(".mdx"))
61
+ continue;
62
+ const path = resolveSpec(spec, ctx);
63
+ if (path)
64
+ out.push({ name, resolvedPath: path });
65
+ }
66
+ return out;
67
+ }
68
+ /** `path` relative to the folder `root` (corpus paths), or undefined when it is not inside it. */
69
+ function inside(path, root) {
70
+ if (!root)
71
+ return path;
72
+ return path.startsWith(`${root}/`) ? path.slice(root.length + 1) : undefined;
73
+ }
74
+ /**
75
+ * Map a partial (a corpus path, or an absolute path in an `--include-root` folder) to its
76
+ * include path, relative to the instance folder. A partial elsewhere in the project is stored
77
+ * under `_project/`, one from an `--include-root` folder under `_include-roots/<folder name>/`
78
+ * (the CLI places it, by real path): relative to the instance either would climb out of the
79
+ * content root and be written outside the output. Undefined when the partial is in none of
80
+ * them (it is then not collected).
81
+ */
82
+ export function fragmentIncludePath(path, ctx) {
83
+ let rel;
84
+ if (path.startsWith("/"))
85
+ rel = ctx.external?.storagePath(path);
86
+ else {
87
+ rel = ctx.instanceRoot !== undefined ? inside(path, ctx.instanceRoot) : undefined;
88
+ if (rel === undefined) {
89
+ const inProject = inside(path, ctx.projectRoot ?? "");
90
+ if (inProject !== undefined)
91
+ rel = `_project/${inProject}`;
92
+ }
93
+ }
94
+ return rel === undefined ? undefined : rel.replace(/\.mdx$/, ".md");
95
+ }
96
+ // ── export const → {% set %} ────────────────────────────
97
+ function rewriteExportConsts(source) {
98
+ // `export const platform='kubernetes';` → `{% set platform = "kubernetes" %}`
99
+ return source.replace(/^\s*export\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:"([^"]*)"|'([^']*)')\s*;?\s*$/gm, (_m, name, dq, sq) => `{% set ${name} = "${dq ?? sq}" %}`);
100
+ }
101
+ // ── <If> → {% if %} (with fallback signalling) ──────────
102
+ /**
103
+ * Translate `<If condition={…}>…</If>` blocks to `{% if … %}…{% endif %}`.
104
+ * Returns the rewritten source plus a flag set when any block could not be
105
+ * translated (caller can decide to fall back to inline for that page/partial).
106
+ */
107
+ export function rewriteIfBlocks(source) {
108
+ let untranslated = 0;
109
+ // Pass 1 — inline `<If cond>content</If>` (open + close on ONE line): a prose
110
+ // sentence or a single <td> cell. Emit an inline `{% if %}…{% endif %}` so the
111
+ // line (and any <td>/<tr> it sits in) stays intact.
112
+ const inlineRe = /<If\s+condition=\{([^}]*)\}\s*>([^\n]*?)<\/If>/g;
113
+ let text = source.replace(inlineRe, (whole, cond, inner) => {
114
+ const minja = jsxConditionToMinja(cond);
115
+ if (minja == null) {
116
+ untranslated++;
117
+ return whole; // leave for the If component-map strip
118
+ }
119
+ return `{% if ${minja} %}${inner}{% endif %}`;
120
+ });
121
+ // Pass 2 — block `<If>` on its own line (wraps block content: <tr> rows, list
122
+ // items). Table-aware: INSIDE an HTML <table>, emit flush directives with NO
123
+ // blank-line padding — blank lines would split the <table> into separate HTML
124
+ // blocks and the indented <tr> rows would parse as literal code. Minja still
125
+ // resolves the rows before markdown-it sees the table. OUTSIDE a table, emit
126
+ // flush + blank-padded so block conditionals don't fuse with markdown lists.
127
+ const lines = text.split("\n");
128
+ const out = [];
129
+ let tableDepth = 0;
130
+ const stack = [];
131
+ const openLine = /^[ \t]*<If\s+condition=\{([^}]*)\}\s*>[ \t]*$/;
132
+ const closeLine = /^[ \t]*<\/If>[ \t]*$/;
133
+ for (const line of lines) {
134
+ const m = openLine.exec(line);
135
+ if (m) {
136
+ const minja = jsxConditionToMinja(m[1]);
137
+ const inTable = tableDepth > 0;
138
+ if (minja == null) {
139
+ untranslated++;
140
+ stack.push({ ok: false, inTable });
141
+ out.push("<If>");
142
+ continue;
143
+ }
144
+ stack.push({ ok: true, inTable });
145
+ // Inside a table, use Minja whitespace control (`{%- … -%}`) so resolving
146
+ // the directive leaves NO blank line — a blank line would fragment the
147
+ // <table> and the indented <tr> rows would parse as an indented code block.
148
+ if (inTable)
149
+ out.push(`{%- if ${minja} -%}`);
150
+ else
151
+ out.push("", `{% if ${minja} %}`, "");
152
+ continue;
153
+ }
154
+ if (closeLine.test(line)) {
155
+ const rec = stack.pop();
156
+ if (!rec || !rec.ok) {
157
+ out.push("</If>"); // untranslated → keep for the If component-map strip
158
+ continue;
159
+ }
160
+ if (rec.inTable)
161
+ out.push("{%- endif -%}");
162
+ else
163
+ out.push("", "{% endif %}", "");
164
+ continue;
165
+ }
166
+ out.push(line);
167
+ tableDepth += (line.match(/<table\b/g) ?? []).length;
168
+ tableDepth -= (line.match(/<\/table>/g) ?? []).length;
169
+ if (tableDepth < 0)
170
+ tableDepth = 0;
171
+ }
172
+ return { text: out.join("\n"), untranslated };
173
+ }
174
+ // ── Partial usage → {% include %} + fragment collection ─
175
+ function emitIncludes(source, imports, ctx) {
176
+ // The directory of the CURRENT file's output (.md), relative to the instance
177
+ // root. Include paths are emitted relative to this, because the build-time
178
+ // Minja preprocessor resolves `{% include %}` against the consuming file's dir
179
+ // (`basePath: dirname(sourcePath)`), not the content root.
180
+ const currentOut = ctx.file !== undefined ? fragmentIncludePath(ctx.file, ctx) : undefined;
181
+ const currentOutDir = currentOut === undefined ? "" : posix.dirname(currentOut).replace(/^\.$/, "");
182
+ let result = source;
183
+ for (const imp of imports) {
184
+ // Stable storage path (content-root-relative) — the exporter writes the
185
+ // fragment here, and it's the fragments-collector key.
186
+ const storagePath = fragmentIncludePath(imp.resolvedPath, ctx);
187
+ // A partial with nowhere inside the output to live is not collected: its usage stays
188
+ // as written and is reported as an unmapped component
189
+ if (storagePath === undefined)
190
+ continue;
191
+ ctx.fragments?.set(imp.resolvedPath, storagePath);
192
+ // Page-relative directive path (e.g. ../../_includes/partials/_x.md).
193
+ let rel = relativeDir(currentOutDir, storagePath);
194
+ if (!rel.startsWith("."))
195
+ rel = `./${rel}`;
196
+ const selfClosing = new RegExp(`<${imp.name}((?:\\s[^>]*?)?)/>`, "g");
197
+ const paired = new RegExp(`<${imp.name}((?:\\s[^>]*?)?)>[\\s\\S]*?</${imp.name}>`, "g");
198
+ const directiveFor = (attrText) => {
199
+ const sets = propSetDirectives(attrText);
200
+ return `\n\n${sets}{% include "${rel}" %}\n\n`;
201
+ };
202
+ result = result
203
+ .replace(paired, (_m, attrs) => directiveFor(attrs))
204
+ .replace(selfClosing, (_m, attrs) => directiveFor(attrs));
205
+ }
206
+ return result;
207
+ }
208
+ /**
209
+ * Emit `{% set %}` lines only for props that need them: a literal value, or a
210
+ * prop whose name differs from the forwarded variable. `name={name}` (same name,
211
+ * the Tigera pattern) needs nothing — the page-level set is already in scope.
212
+ */
213
+ function propSetDirectives(attrText) {
214
+ const sets = [];
215
+ const attrRe = /([A-Za-z_$][\w$]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([^}]*)\})/g;
216
+ let m;
217
+ while ((m = attrRe.exec(attrText)) !== null) {
218
+ const [, name, dq, sq, expr] = m;
219
+ if (dq !== undefined || sq !== undefined) {
220
+ sets.push(`{% set ${name} = "${dq ?? sq}" %}\n`);
221
+ }
222
+ else if (expr !== undefined) {
223
+ const e = expr.trim();
224
+ if (e !== name)
225
+ sets.push(`{% set ${name} = ${e} %}\n`); // rename only
226
+ // same-name forward (e === name) → inherited, no set needed
227
+ }
228
+ }
229
+ return sets.join("");
230
+ }
231
+ // ── Entry point ─────────────────────────────────────────
232
+ /**
233
+ * Apply the structural transform: emit Minja directives + collect fragments.
234
+ * Used for both pages and (recursively, via the importer draining
235
+ * `ctx.fragments`) the partial fragments themselves.
236
+ */
237
+ export function toStructural(source, ctx) {
238
+ const imports = parseMdxImports(source, ctx);
239
+ let text = rewriteExportConsts(source);
240
+ text = rewriteIfBlocks(text).text;
241
+ text = emitIncludes(text, imports, ctx);
242
+ return text;
243
+ }
@@ -0,0 +1,18 @@
1
+ import { type PreprocessContext } from "@dogsbay/format-docusaurus/adapter";
2
+ /**
3
+ * Substitute `$[path]` tokens using `vars`. Unknown tokens and tokens that
4
+ * resolve to non-primitive values are left untouched (and reported via onWarn).
5
+ */
6
+ export declare function substituteVariables(source: string, vars: Record<string, unknown>, onWarn?: (token: string) => void): string;
7
+ /**
8
+ * Load `variables.js` from a product/version folder (a CommonJS data module that may
9
+ * `require('./releases.json')`), through the corpus. Cached per corpus and folder; an empty
10
+ * object when absent.
11
+ *
12
+ * READ as data by default (`@dogsbay/literal`, through format-docusaurus's `readDataModule`):
13
+ * on the Tigera repo that is 23 of 24 calico keys and every calico-cloud key; a value computed
14
+ * with `.split()`/`.slice()` is left unresolved and named, and its `$[token]` stays as written.
15
+ * With `--eval-config` the CLI RUNS the file instead (`ctx.runModule`) — code from the source
16
+ * repo, for a repo you trust.
17
+ */
18
+ export declare function loadVariables(ctx: Pick<PreprocessContext, "corpus" | "instanceRoot" | "projectRoot" | "runModule" | "warn">): Record<string, unknown>;
@@ -0,0 +1,80 @@
1
+ import { readDataModule } from "@dogsbay/format-docusaurus/adapter";
2
+ // Tokens may use array indices (`releases.0`) and keys containing slashes
3
+ // (`components.calico/node.version`), so the charset includes digits and `/`.
4
+ const TOKEN_RE = /\$\[([a-zA-Z0-9_./]+)\]/g;
5
+ /** Resolve a dotted path (e.g. "tigeraOperator.version") within an object. */
6
+ function resolvePath(vars, path) {
7
+ return path.split(".").reduce((acc, key) => {
8
+ if (acc && typeof acc === "object" && key in acc) {
9
+ return acc[key];
10
+ }
11
+ return undefined;
12
+ }, vars);
13
+ }
14
+ /**
15
+ * Substitute `$[path]` tokens using `vars`. Unknown tokens and tokens that
16
+ * resolve to non-primitive values are left untouched (and reported via onWarn).
17
+ */
18
+ export function substituteVariables(source, vars, onWarn) {
19
+ return source.replace(TOKEN_RE, (whole, path) => {
20
+ const value = resolvePath(vars, path);
21
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
22
+ return String(value);
23
+ }
24
+ onWarn?.(path);
25
+ return whole;
26
+ });
27
+ }
28
+ /**
29
+ * Variables read per corpus and folder. Per corpus, not per process: a browser converts one
30
+ * repo after another, and a folder name (`calico`) means a different file in each.
31
+ */
32
+ const cache = new WeakMap();
33
+ /**
34
+ * Load `variables.js` from a product/version folder (a CommonJS data module that may
35
+ * `require('./releases.json')`), through the corpus. Cached per corpus and folder; an empty
36
+ * object when absent.
37
+ *
38
+ * READ as data by default (`@dogsbay/literal`, through format-docusaurus's `readDataModule`):
39
+ * on the Tigera repo that is 23 of 24 calico keys and every calico-cloud key; a value computed
40
+ * with `.split()`/`.slice()` is left unresolved and named, and its `$[token]` stays as written.
41
+ * With `--eval-config` the CLI RUNS the file instead (`ctx.runModule`) — code from the source
42
+ * repo, for a repo you trust.
43
+ */
44
+ export function loadVariables(ctx) {
45
+ const corpus = ctx.corpus;
46
+ if (!corpus || ctx.instanceRoot === undefined)
47
+ return {};
48
+ let byFolder = cache.get(corpus);
49
+ if (!byFolder)
50
+ cache.set(corpus, (byFolder = new Map()));
51
+ const key = `${ctx.runModule ? "run" : "read"}:${ctx.instanceRoot}`;
52
+ const hit = byFolder.get(key);
53
+ if (hit)
54
+ return hit;
55
+ const file = ctx.instanceRoot ? `${ctx.instanceRoot}/variables.js` : "variables.js";
56
+ let vars = {};
57
+ if (corpus.paths.includes(file))
58
+ vars = ctx.runModule ? runVariables(file, ctx) : readVariables(file, corpus, ctx);
59
+ byFolder.set(key, vars);
60
+ return vars;
61
+ }
62
+ function runVariables(file, ctx) {
63
+ try {
64
+ const mod = ctx.runModule(file);
65
+ return mod && typeof mod === "object" ? mod : {};
66
+ }
67
+ catch (err) {
68
+ ctx.warn?.(`Warning: could not load ${file} (${err.message}); $[vars] left as-is.`);
69
+ return {};
70
+ }
71
+ }
72
+ function readVariables(file, corpus, ctx) {
73
+ const { value, unresolved } = readDataModule(corpus, file, { root: ctx.projectRoot ?? "", fallbackExport: "variables" });
74
+ if (unresolved.length) {
75
+ ctx.warn?.(`Warning: ${file}: ${unresolved.length} value(s) could not be read as data and are left out ` +
76
+ `(their $[tokens] stay as written; --eval-config runs the file instead):\n` +
77
+ unresolved.map((u) => ` ${u.startsWith(`${file}: `) ? u.slice(file.length + 2) : u}`).join("\n"));
78
+ }
79
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
80
+ }
package/package.json CHANGED
@@ -1,6 +1,33 @@
1
1
  {
2
2
  "name": "@dogsbay/adapter-tigera",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.2.0-beta.135",
4
+ "description": "Tigera/Calico site adapter for @dogsbay/format-docusaurus — resolves $[variable] substitution and Tigera-specific MDX components.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md"
12
+ ],
13
+ "dependencies": {
14
+ "@dogsbay/corpus": "0.2.0-beta.135",
15
+ "@dogsbay/format-docusaurus": "0.2.0-beta.135"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.20.1",
19
+ "esbuild": "^0.25.0",
20
+ "typescript": "^5.9.3",
21
+ "vitest": "^4.1.10"
22
+ },
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/dogsbay/dogsbay.git",
27
+ "directory": "packages/adapter-tigera"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "test": "vitest run"
32
+ }
6
33
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.