@contentrain/astro-kit 0.1.0

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 (63) hide show
  1. package/README.md +89 -0
  2. package/catalog.json +1780 -0
  3. package/components/_shared/Button.astro +37 -0
  4. package/components/_shared/KitImage.astro +21 -0
  5. package/components/_shared/Section.astro +43 -0
  6. package/components/_shared/types.ts +26 -0
  7. package/components/breadcrumb/Breadcrumb.astro +56 -0
  8. package/components/breadcrumb/fixtures.json +5 -0
  9. package/components/breadcrumb/meta.json +27 -0
  10. package/components/card-grid/CardGrid.astro +82 -0
  11. package/components/card-grid/fixtures.json +6 -0
  12. package/components/card-grid/meta.json +123 -0
  13. package/components/contact-form/ContactForm.astro +79 -0
  14. package/components/contact-form/fixtures.json +18 -0
  15. package/components/contact-form/meta.json +28 -0
  16. package/components/cta/Cta.astro +64 -0
  17. package/components/cta/fixtures.json +6 -0
  18. package/components/cta/meta.json +28 -0
  19. package/components/faq/Faq.astro +79 -0
  20. package/components/faq/fixtures.json +36 -0
  21. package/components/faq/meta.json +30 -0
  22. package/components/footer/Footer.astro +87 -0
  23. package/components/footer/fixtures.json +5 -0
  24. package/components/footer/meta.json +31 -0
  25. package/components/gallery/Gallery.astro +88 -0
  26. package/components/gallery/fixtures.json +191 -0
  27. package/components/gallery/meta.json +30 -0
  28. package/components/header/Header.astro +62 -0
  29. package/components/header/fixtures.json +125 -0
  30. package/components/header/meta.json +34 -0
  31. package/components/hero/Hero.astro +84 -0
  32. package/components/hero/fixtures.json +95 -0
  33. package/components/hero/meta.json +127 -0
  34. package/components/nav/Nav.astro +102 -0
  35. package/components/nav/fixtures.json +80 -0
  36. package/components/nav/meta.json +30 -0
  37. package/components/pagination/Pagination.astro +69 -0
  38. package/components/pagination/fixtures.json +6 -0
  39. package/components/pagination/meta.json +31 -0
  40. package/components/post-card/PostCard.astro +67 -0
  41. package/components/post-card/fixtures.json +54 -0
  42. package/components/post-card/meta.json +32 -0
  43. package/components/prose/Prose.astro +30 -0
  44. package/components/prose/fixtures.json +4 -0
  45. package/components/prose/meta.json +69 -0
  46. package/components/slider/Slider.astro +136 -0
  47. package/components/slider/fixtures.json +5 -0
  48. package/components/slider/meta.json +32 -0
  49. package/components/tabs/Tabs.astro +101 -0
  50. package/components/tabs/fixtures.json +37 -0
  51. package/components/tabs/meta.json +28 -0
  52. package/components/testimonial/Testimonial.astro +70 -0
  53. package/components/testimonial/fixtures.json +26 -0
  54. package/components/testimonial/meta.json +97 -0
  55. package/dist/index.d.mts +150 -0
  56. package/dist/index.d.mts.map +1 -0
  57. package/dist/index.mjs +199 -0
  58. package/dist/index.mjs.map +1 -0
  59. package/mapping/classic.json +261 -0
  60. package/mapping/divi.json +295 -0
  61. package/mapping/elementor.json +322 -0
  62. package/mapping/gutenberg.json +395 -0
  63. package/package.json +63 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,199 @@
1
+ import { copyFile, mkdir, readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/catalog.ts
5
+ const CATALOG_FORMAT = "astro-kit-catalog@1";
6
+ const KIT_BUILDERS = [
7
+ "gutenberg",
8
+ "elementor",
9
+ "divi",
10
+ "classic"
11
+ ];
12
+ /** Element-name prefix per builder, as `facts.json` names elements (`core/cover`, `elementor/heading`, `divi/et_pb_blurb`). */
13
+ const BUILDER_PREFIX = {
14
+ gutenberg: "core/",
15
+ elementor: "elementor/",
16
+ divi: "divi/",
17
+ classic: "classic/"
18
+ };
19
+ /**
20
+ * Whether an element name belongs to a builder. Gutenberg blocks are namespaced by their plugin
21
+ * (`core/cover`, `yoast-seo/breadcrumbs`, `uagb/container`), so any `<ns>/<name>` that is not another
22
+ * builder's is a Gutenberg block.
23
+ */
24
+ function isBuilderElement(builder, name) {
25
+ if (builder !== "gutenberg") return name.startsWith(BUILDER_PREFIX[builder]);
26
+ return /^[a-z0-9-]+\/[a-z0-9-]+(:[\w-]+)?$/.test(name) && !KIT_BUILDERS.some((b) => b !== "gutenberg" && name.startsWith(BUILDER_PREFIX[b]));
27
+ }
28
+ /** Problems with a catalog, one line each; empty when it is sound. */
29
+ function validateCatalog(catalog) {
30
+ const problems = [];
31
+ const ids = new Set(catalog.components.map((c) => c.id));
32
+ for (const c of catalog.components) {
33
+ const at = `component ${c.id}`;
34
+ if (!/^[a-z][a-z0-9-]*$/.test(c.id)) problems.push(`${at}: id must be kebab-case`);
35
+ if (!c.files.length) problems.push(`${at}: no files`);
36
+ for (const used of c.uses) if (!ids.has(used)) problems.push(`${at}: uses unknown component "${used}"`);
37
+ for (const [axis, variant] of Object.entries(c.variants)) if (!variant.options.includes(variant.default)) problems.push(`${at}: variant ${axis} default "${variant.default}" is not an option`);
38
+ for (const [name, prop] of Object.entries(c.props)) {
39
+ if (!prop.description) problems.push(`${at}: prop ${name} has no description`);
40
+ if (prop.type === "select" && !prop.options?.length) problems.push(`${at}: select prop ${name} has no options`);
41
+ }
42
+ for (const [builder, names] of Object.entries(c.sources)) {
43
+ const prefix = BUILDER_PREFIX[builder];
44
+ for (const name of names) if (!isBuilderElement(builder, name)) problems.push(`${at}: source "${name}" is not a ${builder} element (${prefix}…)`);
45
+ }
46
+ }
47
+ const seen = /* @__PURE__ */ new Set();
48
+ for (const c of catalog.components) {
49
+ if (seen.has(c.id)) problems.push(`component ${c.id}: listed twice`);
50
+ seen.add(c.id);
51
+ }
52
+ return problems;
53
+ }
54
+ /** Components that replace a builder element, e.g. `core/cover` → hero. */
55
+ function componentsForSource(catalog, element) {
56
+ return catalog.components.filter((c) => Object.values(c.sources).some((names) => names?.includes(element)));
57
+ }
58
+ //#endregion
59
+ //#region src/copy.ts
60
+ /** Where kit files land in a site, relative to the site root. */
61
+ const KIT_DIR = "src/components/kit";
62
+ function planCopy(catalog, ids) {
63
+ const byId = new Map(catalog.components.map((c) => [c.id, c]));
64
+ const wanted = /* @__PURE__ */ new Set();
65
+ const visit = (id) => {
66
+ if (wanted.has(id)) return;
67
+ const component = byId.get(id);
68
+ if (!component) throw new Error(`astro-kit has no component "${id}". Known: ${[...byId.keys()].join(", ")}.`);
69
+ wanted.add(id);
70
+ for (const used of component.uses) visit(used);
71
+ };
72
+ for (const id of ids) visit(id);
73
+ const files = {};
74
+ const dependencies = { ...catalog.dependencies };
75
+ for (const id of [...wanted].toSorted()) {
76
+ const component = byId.get(id);
77
+ for (const file of component.files) files[`${KIT_DIR}/${id}/${file}`] = `${id}/${file}`;
78
+ for (const file of component.shared) files[`${KIT_DIR}/_shared/${file}`] = `_shared/${file}`;
79
+ Object.assign(dependencies, component.dependencies);
80
+ }
81
+ return {
82
+ components: [...wanted].toSorted(),
83
+ files,
84
+ dependencies
85
+ };
86
+ }
87
+ /** Carry out a plan: `kitRoot` is the kit's `components/` directory, `siteRoot` the site. */
88
+ async function copyComponents(plan, kitRoot, siteRoot) {
89
+ await Promise.all(Object.entries(plan.files).map(async ([dest, source]) => {
90
+ const target = join(siteRoot, dest);
91
+ await mkdir(dirname(target), { recursive: true });
92
+ await copyFile(join(kitRoot, source), target);
93
+ }));
94
+ }
95
+ //#endregion
96
+ //#region src/mapping.ts
97
+ const MAPPING_FORMAT = "astro-kit-mapping@1";
98
+ const MAPPING_VALUE_PATTERN = /^(?:field:[\w-]+|media:[\w-]+|ref:[\w-]+\.[\w-]+|href:(?:self|[\w-]+)|term:[\w-]+|ui:[\w.-]+|const:(?:true|false|-?\d+(?:\.\d+)?|[a-z][a-z0-9_-]{0,31})|site:[\w-]+|menu:(?:primary|footer)|page:(?:base|current|total|breadcrumb)|attr:[\w.-]+|(?:dom|img|link|html):[^@]*(?:@[\w-]+)?)$/;
99
+ /** Problems with a mapping table against the catalog, one line each; empty when it is sound. */
100
+ function validateMapping(table, catalog) {
101
+ const problems = [];
102
+ if (table.format !== "astro-kit-mapping@1") problems.push(`format is not ${MAPPING_FORMAT}`);
103
+ if (!KIT_BUILDERS.includes(table.builder)) problems.push(`unknown builder ${table.builder}`);
104
+ if (!/^\d+$/.test(table.version)) problems.push("version is not an integer string");
105
+ const components = new Map(catalog.components.map((c) => [c.id, c]));
106
+ const seen = /* @__PURE__ */ new Set();
107
+ for (const rule of table.rules) {
108
+ const at = `${table.builder} ${rule.match}${rule.when ? " (when)" : ""}`;
109
+ const key = `${rule.match}|${JSON.stringify(rule.when ?? {})}`;
110
+ if (seen.has(key)) problems.push(`${at}: matched twice`);
111
+ seen.add(key);
112
+ if (!isBuilderElement(table.builder, rule.match)) problems.push(`${at}: not a ${table.builder} element`);
113
+ const c = components.get(rule.component);
114
+ if (!c) {
115
+ problems.push(`${at}: component ${rule.component} is not in the catalog`);
116
+ continue;
117
+ }
118
+ for (const [axis, value] of Object.entries(rule.variant ?? {})) if (!c.variants[axis]) problems.push(`${at}: ${c.id} has no variant axis ${axis}`);
119
+ else if (!c.variants[axis].options.includes(value)) problems.push(`${at}: ${c.id}.${axis} has no option ${value}`);
120
+ for (const [prop, value] of Object.entries({
121
+ ...rule.props,
122
+ ...rule.into ? {} : rule.item
123
+ })) {
124
+ if (!c.props[prop]) problems.push(`${at}: ${c.id} has no prop ${prop}`);
125
+ if (!MAPPING_VALUE_PATTERN.test(value)) problems.push(`${at}: ${prop} = ${value} is not a mapping value`);
126
+ }
127
+ if (rule.into) {
128
+ const target = c.props[rule.into];
129
+ if (!target || target.type !== "array") problems.push(`${at}: ${c.id}.${rule.into} is not an array prop`);
130
+ const fields = target && typeof target.items === "object" ? target.items.fields ?? {} : {};
131
+ for (const [field, value] of Object.entries(rule.item ?? {})) {
132
+ if (!fields[field]) problems.push(`${at}: ${c.id}.${rule.into}[] has no field ${field}`);
133
+ if (!MAPPING_VALUE_PATTERN.test(value)) problems.push(`${at}: ${rule.into}[].${field} = ${value} is not a mapping value`);
134
+ }
135
+ if (!rule.each && !rule.group && !rule.bind) problems.push(`${at}: into ${rule.into} needs each, group or bind to say what repeats`);
136
+ }
137
+ if (rule.item && !rule.into && !rule.bind) problems.push(`${at}: item without into or bind`);
138
+ const bound = new Set([...Object.keys(rule.props ?? {}), ...rule.into ? [rule.into] : Object.keys(rule.item ?? {})]);
139
+ for (const [prop, def] of Object.entries(c.props)) if (def.required && !bound.has(prop)) problems.push(`${at}: required prop ${c.id}.${prop} has no source`);
140
+ }
141
+ return problems;
142
+ }
143
+ /** Catalog sources of a builder that no rule of its table matches. */
144
+ function unmappedSources(table, catalog) {
145
+ const matched = new Set(table.rules.map((r) => r.match));
146
+ const sources = catalog.components.flatMap((c) => c.sources[table.builder] ?? []);
147
+ return [...new Set(sources)].filter((s) => !matched.has(s)).toSorted();
148
+ }
149
+ /** Rules for an element, most specific (with `when` / qualifier) first. */
150
+ function rulesFor(table, element) {
151
+ return table.rules.filter((r) => r.match === element || r.match.split(":")[0] === element).toSorted((a, b) => Number(!!b.when || b.match.includes(":")) - Number(!!a.when || a.match.includes(":")));
152
+ }
153
+ /** Whether a rule's `match` (with an optional `:qualifier`) and `when.attr` fit an element. */
154
+ function ruleMatches(rule, node) {
155
+ const [name, qualifier] = rule.match.split(":");
156
+ if (name !== node.name) return false;
157
+ const attrs = node.attrs ?? {};
158
+ if (qualifier && ![
159
+ attrs.area,
160
+ attrs.slug,
161
+ attrs.tagName,
162
+ attrs.qualifier
163
+ ].includes(qualifier)) return false;
164
+ for (const [key, value] of Object.entries(rule.when?.attr ?? {})) if (attrs[key] !== value) return false;
165
+ if (rule.when?.class && !String(attrs.className ?? attrs.classes ?? "").split(/\s+/).includes(rule.when.class)) return false;
166
+ return true;
167
+ }
168
+ /**
169
+ * Apply a table to a tree: the outermost matching element owns its whole subtree, and nothing inside
170
+ * it is matched again — a header template part holding the site title and logo is one header, not three.
171
+ */
172
+ function claimMatches(nodes, table, path = "0") {
173
+ return nodes.flatMap((node, i) => {
174
+ const at = `${path}.${i}`;
175
+ const rule = rulesFor(table, node.name).find((r) => ruleMatches(r, node));
176
+ return rule ? [{
177
+ path: at,
178
+ node,
179
+ rule
180
+ }] : claimMatches(node.children ?? [], table, at);
181
+ });
182
+ }
183
+ //#endregion
184
+ //#region src/index.ts
185
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
186
+ /** The kit's `components/` directory — the source `copyComponents` copies from. */
187
+ const KIT_COMPONENTS_DIR = join(PACKAGE_ROOT, "components");
188
+ /** A builder's mapping table (`mapping/<builder>.json`). */
189
+ async function loadMapping(builder) {
190
+ return JSON.parse(await readFile(join(PACKAGE_ROOT, "mapping", `${builder}.json`), "utf8"));
191
+ }
192
+ /** The published catalog (`catalog.json`). */
193
+ async function loadCatalog() {
194
+ return JSON.parse(await readFile(join(PACKAGE_ROOT, "catalog.json"), "utf8"));
195
+ }
196
+ //#endregion
197
+ export { BUILDER_PREFIX, CATALOG_FORMAT, KIT_BUILDERS, KIT_COMPONENTS_DIR, KIT_DIR, MAPPING_FORMAT, MAPPING_VALUE_PATTERN, claimMatches, componentsForSource, copyComponents, isBuilderElement, loadCatalog, loadMapping, planCopy, ruleMatches, rulesFor, unmappedSources, validateCatalog, validateMapping };
198
+
199
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/catalog.ts","../src/copy.ts","../src/mapping.ts","../src/index.ts"],"sourcesContent":["// The kit catalog: what each component is, what it takes, how it varies and\n// which WordPress builder elements it stands in for. Machine-readable, so the\n// migration writer chooses components by querying it instead of reading\n// source, and the builder mapping tables (mapping/*.json) are checked against\n// it.\n//\n// Props are described with Contentrain's own `FieldDef`, so a model field and\n// the prop it feeds are compared type to type.\n\nimport type { FieldDef } from '@contentrain/types'\n\nexport const CATALOG_FORMAT = 'astro-kit-catalog@1'\n\n/** The WordPress builders whose elements a component is fed from. */\nexport type KitBuilder = 'gutenberg' | 'elementor' | 'divi' | 'classic'\nexport const KIT_BUILDERS: readonly KitBuilder[] = ['gutenberg', 'elementor', 'divi', 'classic']\n\n/** Element-name prefix per builder, as `facts.json` names elements (`core/cover`, `elementor/heading`, `divi/et_pb_blurb`). */\nexport const BUILDER_PREFIX: Record<KitBuilder, string> = {\n gutenberg: 'core/',\n elementor: 'elementor/',\n divi: 'divi/',\n classic: 'classic/',\n}\n\n/**\n * Whether an element name belongs to a builder. Gutenberg blocks are namespaced by their plugin\n * (`core/cover`, `yoast-seo/breadcrumbs`, `uagb/container`), so any `<ns>/<name>` that is not another\n * builder's is a Gutenberg block.\n */\nexport function isBuilderElement(builder: KitBuilder, name: string): boolean {\n if (builder !== 'gutenberg') return name.startsWith(BUILDER_PREFIX[builder])\n return /^[a-z0-9-]+\\/[a-z0-9-]+(:[\\w-]+)?$/.test(name) && !KIT_BUILDERS.some(b => b !== 'gutenberg' && name.startsWith(BUILDER_PREFIX[b]))\n}\n\nexport type KitCategory = 'chrome' | 'section' | 'content' | 'primitive'\n\n/** Client JavaScript a component ships: none, Embla (carousel), Zag.js (accessible widget) or the Studio runtime. */\nexport type KitJs = 'none' | 'embla' | 'zag' | 'studio'\n\nexport type KitProp = FieldDef & { description: string }\n\nexport interface KitVariant {\n options: string[]\n default: string\n description: string\n}\n\n/** One component as written in `components/<id>/meta.json`. */\nexport interface KitComponentMeta {\n id: string\n name: string\n description: string\n category: KitCategory\n /** Kit components this one renders. */\n uses: string[]\n /** Files it imports from `components/_shared/`. */\n shared: string[]\n variants: Record<string, KitVariant>\n props: Record<string, KitProp>\n /** Named slots → what goes in them. `default` is the unnamed slot. */\n slots: Record<string, string>\n js: KitJs\n /** npm packages the site needs for this component, name → range. */\n dependencies: Record<string, string>\n /** Builder elements this component replaces, by builder — what the mapping tables map from. */\n sources: Partial<Record<KitBuilder, string[]>>\n /** How it meets its accessibility pattern. */\n a11y: string\n}\n\n/** A catalog entry: the meta plus the files that make the component. */\nexport interface KitComponent extends KitComponentMeta {\n /** Paths relative to `components/<id>/`. */\n files: string[]\n}\n\nexport interface KitCatalog {\n format: typeof CATALOG_FORMAT\n /** Design tokens (Tailwind @theme names) the components read; the site must define them. */\n tokens: string[]\n /** npm packages every kit component needs. */\n dependencies: Record<string, string>\n components: KitComponent[]\n}\n\n/** Problems with a catalog, one line each; empty when it is sound. */\nexport function validateCatalog(catalog: KitCatalog): string[] {\n const problems: string[] = []\n const ids = new Set(catalog.components.map(c => c.id))\n for (const c of catalog.components) {\n const at = `component ${c.id}`\n if (!/^[a-z][a-z0-9-]*$/.test(c.id)) problems.push(`${at}: id must be kebab-case`)\n if (!c.files.length) problems.push(`${at}: no files`)\n for (const used of c.uses) if (!ids.has(used)) problems.push(`${at}: uses unknown component \"${used}\"`)\n for (const [axis, variant] of Object.entries(c.variants)) {\n if (!variant.options.includes(variant.default)) problems.push(`${at}: variant ${axis} default \"${variant.default}\" is not an option`)\n }\n for (const [name, prop] of Object.entries(c.props)) {\n if (!prop.description) problems.push(`${at}: prop ${name} has no description`)\n if (prop.type === 'select' && !prop.options?.length) problems.push(`${at}: select prop ${name} has no options`)\n }\n for (const [builder, names] of Object.entries(c.sources) as Array<[KitBuilder, string[]]>) {\n const prefix = BUILDER_PREFIX[builder]\n for (const name of names) if (!isBuilderElement(builder, name)) problems.push(`${at}: source \"${name}\" is not a ${builder} element (${prefix}…)`)\n }\n }\n const seen = new Set<string>()\n for (const c of catalog.components) {\n if (seen.has(c.id)) problems.push(`component ${c.id}: listed twice`)\n seen.add(c.id)\n }\n return problems\n}\n\n/** Components that replace a builder element, e.g. `core/cover` → hero. */\nexport function componentsForSource(catalog: KitCatalog, element: string): KitComponent[] {\n return catalog.components.filter(c => Object.values(c.sources).some(names => names?.includes(element)))\n}\n","// Copying components into a site — the shadcn model: the site owns the\n// source, there is no runtime dependency on this package. A component is\n// copied with every kit component it renders and every shared file it\n// imports, under `src/components/kit/`, keeping the relative layout its\n// imports expect.\n\nimport { copyFile, mkdir } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport type { KitCatalog } from './catalog.js'\n\n/** Where kit files land in a site, relative to the site root. */\nexport const KIT_DIR = 'src/components/kit'\n\nexport interface CopyPlan {\n /** Components copied, the requested ones and everything they render, sorted. */\n components: string[]\n /** Site-relative destination → path relative to the kit's `components/` directory. */\n files: Record<string, string>\n /** npm packages the site needs, name → range. */\n dependencies: Record<string, string>\n}\n\nexport function planCopy(catalog: KitCatalog, ids: readonly string[]): CopyPlan {\n const byId = new Map(catalog.components.map(c => [c.id, c]))\n const wanted = new Set<string>()\n const visit = (id: string) => {\n if (wanted.has(id)) return\n const component = byId.get(id)\n if (!component) throw new Error(`astro-kit has no component \"${id}\". Known: ${[...byId.keys()].join(', ')}.`)\n wanted.add(id)\n for (const used of component.uses) visit(used)\n }\n for (const id of ids) visit(id)\n\n const files: Record<string, string> = {}\n const dependencies: Record<string, string> = { ...catalog.dependencies }\n for (const id of [...wanted].toSorted()) {\n const component = byId.get(id)!\n for (const file of component.files) files[`${KIT_DIR}/${id}/${file}`] = `${id}/${file}`\n for (const file of component.shared) files[`${KIT_DIR}/_shared/${file}`] = `_shared/${file}`\n Object.assign(dependencies, component.dependencies)\n }\n return { components: [...wanted].toSorted(), files, dependencies }\n}\n\n/** Carry out a plan: `kitRoot` is the kit's `components/` directory, `siteRoot` the site. */\nexport async function copyComponents(plan: CopyPlan, kitRoot: string, siteRoot: string): Promise<void> {\n await Promise.all(Object.entries(plan.files).map(async ([dest, source]) => {\n const target = join(siteRoot, dest)\n await mkdir(dirname(target), { recursive: true })\n await copyFile(join(kitRoot, source), target)\n }))\n}\n","// Builder mapping tables: which kit component a WordPress builder element\n// becomes, and where each of its props comes from. One versioned table per\n// builder (`mapping/<builder>.json`), written once and applied\n// deterministically to every site — the migration model is asked only about\n// elements no rule covers (Migrate's `unmapped_element` decision).\n//\n// A prop's source is one expression from a small closed set. The first six are\n// the project plan's values (`@contentrain/types` `PlanValue`) and bind\n// content; the rest read the builder element itself or the page it is on:\n//\n// field:<name> · media:<field> · ref:<field>.<field> · href:self|<field> · term:<field> · ui:<key> · const:<value>\n// site:<field> a field of the `site` singleton (`site:title`, `site:logo`)\n// menu:primary|footer the menu the starter shows in that area\n// page:<value> what the route knows: `page:base`, `page:current`, `page:total`, `page:breadcrumb`\n// attr:<name> a builder attribute (block attribute, Elementor setting, Divi shortcode attribute)\n// dom:<selector> text of the first match inside the element (empty selector: the element)\n// dom:<selector>@<attr> an attribute of it (`dom:a@href`)\n// img:<selector> an image as the kit's `ImageInput` (src, alt, width, height)\n// link:<selector> a link as `{ label, href }`\n// html:<selector> inner markup, kept as Markdown (rich text)\n\nimport type { KitBuilder, KitCatalog, KitComponent } from './catalog.js'\nimport { isBuilderElement, KIT_BUILDERS } from './catalog.js'\n\nexport const MAPPING_FORMAT = 'astro-kit-mapping@1'\n\nexport const MAPPING_VALUE_PATTERN = /^(?:field:[\\w-]+|media:[\\w-]+|ref:[\\w-]+\\.[\\w-]+|href:(?:self|[\\w-]+)|term:[\\w-]+|ui:[\\w.-]+|const:(?:true|false|-?\\d+(?:\\.\\d+)?|[a-z][a-z0-9_-]{0,31})|site:[\\w-]+|menu:(?:primary|footer)|page:(?:base|current|total|breadcrumb)|attr:[\\w.-]+|(?:dom|img|link|html):[^@]*(?:@[\\w-]+)?)$/\n\nexport interface MappingRule {\n /** Builder element, as the fact pack names it; a `:qualifier` narrows it (`core/template-part:header`). */\n match: string\n /** Extra conditions on the element. */\n when?: { attr?: Record<string, string | number | boolean>, class?: string }\n /** Kit component id. */\n component: string\n variant?: Record<string, string>\n /** Content the element lists rather than holds: one component per entry of the collection (`collection:posts`). */\n bind?: `collection:${string}`\n /** Consecutive sibling matches form one component (a row of Elementor icon boxes is one card grid). */\n group?: 'siblings'\n props?: Record<string, string>\n /** Array prop filled from repeated parts of the element (`each`) or the grouped siblings / bound entries. */\n into?: string\n /** Selector of the repeated parts inside the element, for `into`. */\n each?: string\n /** Props of one repeated part / sibling / entry, with sources relative to it. */\n item?: Record<string, string>\n}\n\nexport interface MappingTable {\n format: typeof MAPPING_FORMAT\n builder: KitBuilder\n /** Bumped on any rule change: plans record the version they applied. */\n version: string\n /** What an element no rule covers becomes when no decision is made: rich text. */\n fallback: 'prose'\n rules: MappingRule[]\n}\n\n/** Problems with a mapping table against the catalog, one line each; empty when it is sound. */\nexport function validateMapping(table: MappingTable, catalog: KitCatalog): string[] {\n const problems: string[] = []\n if (table.format !== MAPPING_FORMAT) problems.push(`format is not ${MAPPING_FORMAT}`)\n if (!KIT_BUILDERS.includes(table.builder)) problems.push(`unknown builder ${table.builder}`)\n if (!/^\\d+$/.test(table.version)) problems.push('version is not an integer string')\n const components = new Map(catalog.components.map(c => [c.id, c]))\n const seen = new Set<string>()\n for (const rule of table.rules) {\n const at = `${table.builder} ${rule.match}${rule.when ? ' (when)' : ''}`\n const key = `${rule.match}|${JSON.stringify(rule.when ?? {})}`\n if (seen.has(key)) problems.push(`${at}: matched twice`)\n seen.add(key)\n if (!isBuilderElement(table.builder, rule.match)) problems.push(`${at}: not a ${table.builder} element`)\n const c = components.get(rule.component)\n if (!c) { problems.push(`${at}: component ${rule.component} is not in the catalog`); continue }\n for (const [axis, value] of Object.entries(rule.variant ?? {})) {\n if (!c.variants[axis]) problems.push(`${at}: ${c.id} has no variant axis ${axis}`)\n else if (!c.variants[axis]!.options.includes(value)) problems.push(`${at}: ${c.id}.${axis} has no option ${value}`)\n }\n for (const [prop, value] of Object.entries({ ...rule.props, ...(rule.into ? {} : rule.item) })) {\n if (!c.props[prop]) problems.push(`${at}: ${c.id} has no prop ${prop}`)\n if (!MAPPING_VALUE_PATTERN.test(value)) problems.push(`${at}: ${prop} = ${value} is not a mapping value`)\n }\n if (rule.into) {\n const target = c.props[rule.into]\n if (!target || target.type !== 'array') problems.push(`${at}: ${c.id}.${rule.into} is not an array prop`)\n const fields = target && typeof target.items === 'object' ? target.items.fields ?? {} : {}\n for (const [field, value] of Object.entries(rule.item ?? {})) {\n if (!fields[field]) problems.push(`${at}: ${c.id}.${rule.into}[] has no field ${field}`)\n if (!MAPPING_VALUE_PATTERN.test(value)) problems.push(`${at}: ${rule.into}[].${field} = ${value} is not a mapping value`)\n }\n if (!rule.each && !rule.group && !rule.bind) problems.push(`${at}: into ${rule.into} needs each, group or bind to say what repeats`)\n }\n if (rule.item && !rule.into && !rule.bind) problems.push(`${at}: item without into or bind`)\n const bound = new Set([...Object.keys(rule.props ?? {}), ...(rule.into ? [rule.into] : Object.keys(rule.item ?? {}))])\n for (const [prop, def] of Object.entries(c.props)) if (def.required && !bound.has(prop)) problems.push(`${at}: required prop ${c.id}.${prop} has no source`)\n }\n return problems\n}\n\n/** Catalog sources of a builder that no rule of its table matches. */\nexport function unmappedSources(table: MappingTable, catalog: KitCatalog): string[] {\n const matched = new Set(table.rules.map(r => r.match))\n const sources = catalog.components.flatMap((c: KitComponent) => c.sources[table.builder] ?? [])\n return [...new Set(sources)].filter(s => !matched.has(s)).toSorted()\n}\n\n/** Rules for an element, most specific (with `when` / qualifier) first. */\nexport function rulesFor(table: MappingTable, element: string): MappingRule[] {\n return table.rules\n .filter(r => r.match === element || r.match.split(':')[0] === element)\n .toSorted((a, b) => Number(!!b.when || b.match.includes(':')) - Number(!!a.when || a.match.includes(':')))\n}\n\n/** A builder element as the fact pack gives it (`FactBlock`): name, attributes, children. */\nexport interface MappingNode {\n name: string\n attrs?: Record<string, unknown>\n children?: MappingNode[]\n}\n\n/** Whether a rule's `match` (with an optional `:qualifier`) and `when.attr` fit an element. */\nexport function ruleMatches(rule: MappingRule, node: MappingNode): boolean {\n const [name, qualifier] = rule.match.split(':')\n if (name !== node.name) return false\n const attrs = node.attrs ?? {}\n // A qualifier names the element's role: a template part's area or slug, or a sub-part the fact pack marks.\n if (qualifier && ![attrs.area, attrs.slug, attrs.tagName, attrs.qualifier].includes(qualifier)) return false\n for (const [key, value] of Object.entries(rule.when?.attr ?? {})) if (attrs[key] !== value) return false\n if (rule.when?.class && !String(attrs.className ?? attrs.classes ?? '').split(/\\s+/).includes(rule.when.class)) return false\n return true\n}\n\n/**\n * Apply a table to a tree: the outermost matching element owns its whole subtree, and nothing inside\n * it is matched again — a header template part holding the site title and logo is one header, not three.\n */\nexport function claimMatches(nodes: MappingNode[], table: MappingTable, path = '0'): { path: string, node: MappingNode, rule: MappingRule }[] {\n return nodes.flatMap((node, i) => {\n const at = `${path}.${i}`\n const rule = rulesFor(table, node.name).find(r => ruleMatches(r, node))\n return rule ? [{ path: at, node, rule }] : claimMatches(node.children ?? [], table, at)\n })\n}\n","import { readFile } from 'node:fs/promises'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport type { KitBuilder, KitCatalog } from './catalog.js'\nimport type { MappingTable } from './mapping.js'\n\nexport * from './catalog.js'\nexport * from './copy.js'\nexport * from './mapping.js'\n\nconst PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')\n\n/** The kit's `components/` directory — the source `copyComponents` copies from. */\nexport const KIT_COMPONENTS_DIR = join(PACKAGE_ROOT, 'components')\n\n/** A builder's mapping table (`mapping/<builder>.json`). */\nexport async function loadMapping(builder: KitBuilder): Promise<MappingTable> {\n return JSON.parse(await readFile(join(PACKAGE_ROOT, 'mapping', `${builder}.json`), 'utf8')) as MappingTable\n}\n\n/** The published catalog (`catalog.json`). */\nexport async function loadCatalog(): Promise<KitCatalog> {\n return JSON.parse(await readFile(join(PACKAGE_ROOT, 'catalog.json'), 'utf8')) as KitCatalog\n}\n"],"mappings":"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"}
@@ -0,0 +1,261 @@
1
+ {
2
+ "format": "astro-kit-mapping@1",
3
+ "builder": "classic",
4
+ "version": "3",
5
+ "fallback": "prose",
6
+ "rules": [
7
+ {
8
+ "match": "classic/header",
9
+ "component": "header",
10
+ "props": {
11
+ "siteName": "site:title",
12
+ "logo": "site:logo",
13
+ "items": "menu:primary",
14
+ "navLabel": "ui:nav.label",
15
+ "menuLabel": "ui:nav.menu",
16
+ "closeLabel": "ui:nav.close"
17
+ }
18
+ },
19
+ {
20
+ "match": "classic/nav-menu",
21
+ "component": "nav",
22
+ "props": {
23
+ "items": "menu:primary",
24
+ "label": "ui:nav.label",
25
+ "menuLabel": "ui:nav.menu",
26
+ "closeLabel": "ui:nav.close"
27
+ }
28
+ },
29
+ {
30
+ "match": "classic/footer",
31
+ "component": "footer",
32
+ "props": {
33
+ "siteName": "site:title",
34
+ "tagline": "site:description",
35
+ "links": "menu:footer",
36
+ "copyright": "ui:footer.copyright",
37
+ "navLabel": "ui:footer.nav"
38
+ }
39
+ },
40
+ {
41
+ "match": "classic/widget-area",
42
+ "component": "footer",
43
+ "props": {
44
+ "siteName": "site:title"
45
+ },
46
+ "into": "columns",
47
+ "each": ".widget",
48
+ "item": {
49
+ "title": "dom:.widget-title,h2,h3"
50
+ }
51
+ },
52
+ {
53
+ "match": "classic/archive-item",
54
+ "component": "post-card",
55
+ "bind": "collection:posts",
56
+ "item": {
57
+ "title": "field:title",
58
+ "href": "href:self",
59
+ "excerpt": "field:excerpt",
60
+ "date": "field:date",
61
+ "image": "media:featured_image",
62
+ "author": "ref:author.name",
63
+ "category": "term:categories"
64
+ }
65
+ },
66
+ {
67
+ "match": "classic/pagination",
68
+ "component": "pagination",
69
+ "props": {
70
+ "base": "page:base",
71
+ "current": "page:current",
72
+ "total": "page:total",
73
+ "label": "ui:pagination.label",
74
+ "prevLabel": "ui:pagination.prev",
75
+ "nextLabel": "ui:pagination.next",
76
+ "pageLabel": "ui:pagination.page"
77
+ }
78
+ },
79
+ {
80
+ "match": "classic/yoast-breadcrumb",
81
+ "component": "breadcrumb",
82
+ "props": {
83
+ "items": "page:breadcrumb",
84
+ "current": "field:title",
85
+ "label": "ui:breadcrumb.label"
86
+ }
87
+ },
88
+ {
89
+ "match": "classic/rank-math-breadcrumb",
90
+ "component": "breadcrumb",
91
+ "props": {
92
+ "items": "page:breadcrumb",
93
+ "current": "field:title",
94
+ "label": "ui:breadcrumb.label"
95
+ }
96
+ },
97
+ {
98
+ "match": "classic/revslider",
99
+ "component": "slider",
100
+ "props": {
101
+ "label": "ui:slider.label",
102
+ "prevLabel": "ui:slider.prev",
103
+ "nextLabel": "ui:slider.next",
104
+ "slideLabel": "ui:slider.slide",
105
+ "ofLabel": "ui:slider.of"
106
+ },
107
+ "into": "slides",
108
+ "each": "rs-slide, .tp-revslider-slidesli",
109
+ "item": {
110
+ "image": "img:img",
111
+ "title": "dom:rs-layer"
112
+ }
113
+ },
114
+ {
115
+ "match": "classic/metaslider",
116
+ "component": "slider",
117
+ "props": {
118
+ "label": "ui:slider.label",
119
+ "prevLabel": "ui:slider.prev",
120
+ "nextLabel": "ui:slider.next",
121
+ "slideLabel": "ui:slider.slide",
122
+ "ofLabel": "ui:slider.of"
123
+ },
124
+ "into": "slides",
125
+ "each": ".slides > li",
126
+ "item": {
127
+ "image": "img:img",
128
+ "text": "dom:.caption"
129
+ }
130
+ },
131
+ {
132
+ "match": "classic/smart-slider",
133
+ "component": "slider",
134
+ "props": {
135
+ "label": "ui:slider.label",
136
+ "prevLabel": "ui:slider.prev",
137
+ "nextLabel": "ui:slider.next",
138
+ "slideLabel": "ui:slider.slide",
139
+ "ofLabel": "ui:slider.of"
140
+ },
141
+ "into": "slides",
142
+ "each": ".n2-ss-slide",
143
+ "item": {
144
+ "image": "img:img",
145
+ "title": "dom:.n2-ss-item-content"
146
+ }
147
+ },
148
+ {
149
+ "match": "classic/hero",
150
+ "component": "hero",
151
+ "props": {
152
+ "heading": "dom:h1,h2",
153
+ "lead": "dom:p",
154
+ "image": "img:img"
155
+ },
156
+ "into": "actions",
157
+ "each": "a.button,a.btn,.wp-block-button__link",
158
+ "item": {
159
+ "label": "dom:",
160
+ "href": "dom:@href"
161
+ }
162
+ },
163
+ {
164
+ "match": "classic/cta",
165
+ "component": "cta",
166
+ "props": {
167
+ "heading": "dom:h2,h3",
168
+ "body": "dom:p"
169
+ },
170
+ "into": "actions",
171
+ "each": "a",
172
+ "item": {
173
+ "label": "dom:",
174
+ "href": "dom:@href"
175
+ }
176
+ },
177
+ {
178
+ "match": "classic/faq",
179
+ "component": "faq",
180
+ "into": "items",
181
+ "each": "details, .faq-item",
182
+ "item": {
183
+ "question": "dom:summary, .faq-question",
184
+ "answer": "html:.faq-answer, :scope > :not(summary)"
185
+ }
186
+ },
187
+ {
188
+ "match": "classic/tabs",
189
+ "component": "tabs",
190
+ "into": "items",
191
+ "each": "[role=tab]",
192
+ "item": {
193
+ "label": "dom:",
194
+ "content": "html:[role=tabpanel]"
195
+ }
196
+ },
197
+ {
198
+ "match": "classic/testimonial",
199
+ "component": "testimonial",
200
+ "group": "siblings",
201
+ "into": "items",
202
+ "item": {
203
+ "quote": "dom:blockquote, .testimonial-text",
204
+ "name": "dom:cite, .testimonial-name",
205
+ "avatar": "img:img"
206
+ }
207
+ },
208
+ {
209
+ "match": "classic/gallery",
210
+ "component": "gallery",
211
+ "props": {
212
+ "closeLabel": "ui:gallery.close"
213
+ },
214
+ "into": "items",
215
+ "each": ".gallery-item",
216
+ "item": {
217
+ "image": "img:img",
218
+ "caption": "dom:.gallery-caption, .wp-caption-text"
219
+ }
220
+ },
221
+ {
222
+ "match": "classic/contact-form-7",
223
+ "component": "contact-form",
224
+ "props": {
225
+ "model": "const:contact",
226
+ "heading": "dom:h2,h3"
227
+ }
228
+ },
229
+ {
230
+ "match": "classic/wpforms",
231
+ "component": "contact-form",
232
+ "props": {
233
+ "model": "const:contact",
234
+ "heading": "dom:h2,h3"
235
+ }
236
+ },
237
+ {
238
+ "match": "classic/gravityforms",
239
+ "component": "contact-form",
240
+ "props": {
241
+ "model": "const:contact",
242
+ "heading": "dom:h2,h3"
243
+ }
244
+ },
245
+ {
246
+ "match": "classic/ninja-forms",
247
+ "component": "contact-form",
248
+ "props": {
249
+ "model": "const:contact",
250
+ "heading": "dom:h2,h3"
251
+ }
252
+ },
253
+ {
254
+ "match": "classic/the-content",
255
+ "component": "prose",
256
+ "props": {
257
+ "html": "html:"
258
+ }
259
+ }
260
+ ]
261
+ }