@contentrain/astro-kit 0.4.0 → 0.6.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 (71) hide show
  1. package/README.md +2 -0
  2. package/catalog.json +1519 -165
  3. package/components/_shared/KitImage.astro +10 -4
  4. package/components/_shared/image.ts +14 -0
  5. package/components/_shared/types.ts +2 -1
  6. package/components/announcement/Announcement.astro +53 -0
  7. package/components/announcement/fixtures.json +6 -0
  8. package/components/announcement/meta.json +53 -0
  9. package/components/card-grid/CardGrid.astro +5 -3
  10. package/components/card-grid/meta.json +25 -11
  11. package/components/consent/Consent.astro +87 -0
  12. package/components/consent/fixtures.json +4 -0
  13. package/components/consent/meta.json +67 -0
  14. package/components/contact-details/ContactDetails.astro +74 -0
  15. package/components/contact-details/fixtures.json +4 -0
  16. package/components/contact-details/meta.json +105 -0
  17. package/components/contact-form/ContactForm.astro +22 -6
  18. package/components/contact-form/fixtures.json +2 -1
  19. package/components/contact-form/meta.json +7 -6
  20. package/components/cta/meta.json +4 -4
  21. package/components/faq/Faq.astro +1 -1
  22. package/components/faq/meta.json +7 -7
  23. package/components/feature-list/FeatureList.astro +85 -0
  24. package/components/feature-list/fixtures.json +129 -0
  25. package/components/feature-list/meta.json +118 -0
  26. package/components/footer/Footer.astro +15 -7
  27. package/components/footer/fixtures.json +3 -1
  28. package/components/footer/meta.json +12 -0
  29. package/components/gallery/Gallery.astro +7 -3
  30. package/components/gallery/meta.json +6 -6
  31. package/components/header/Header.astro +6 -4
  32. package/components/header/fixtures.json +2 -1
  33. package/components/hero/meta.json +30 -8
  34. package/components/logo-cloud/LogoCloud.astro +69 -0
  35. package/components/logo-cloud/fixtures.json +139 -0
  36. package/components/logo-cloud/meta.json +107 -0
  37. package/components/nav/Nav.astro +11 -4
  38. package/components/nav/fixtures.json +1 -0
  39. package/components/nav/meta.json +8 -0
  40. package/components/newsletter/meta.json +12 -0
  41. package/components/pricing/Pricing.astro +94 -0
  42. package/components/pricing/fixtures.json +6 -0
  43. package/components/pricing/meta.json +133 -0
  44. package/components/prose/Prose.astro +7 -3
  45. package/components/prose/fixtures.json +2 -1
  46. package/components/prose/meta.json +5 -2
  47. package/components/slider/Slider.astro +15 -3
  48. package/components/slider/meta.json +8 -8
  49. package/components/split/Split.astro +64 -0
  50. package/components/split/fixtures.json +6 -0
  51. package/components/split/meta.json +154 -0
  52. package/components/stats/Stats.astro +66 -0
  53. package/components/stats/fixtures.json +98 -0
  54. package/components/stats/meta.json +113 -0
  55. package/components/steps/Steps.astro +77 -0
  56. package/components/steps/fixtures.json +5 -0
  57. package/components/steps/meta.json +105 -0
  58. package/components/team/Team.astro +82 -0
  59. package/components/team/fixtures.json +5 -0
  60. package/components/team/meta.json +116 -0
  61. package/components/testimonial/Testimonial.astro +5 -3
  62. package/components/testimonial/meta.json +21 -20
  63. package/dist/index.d.mts +115 -2
  64. package/dist/index.d.mts.map +1 -1
  65. package/dist/index.mjs +175 -3
  66. package/dist/index.mjs.map +1 -1
  67. package/mapping/classic.json +51 -6
  68. package/mapping/divi.json +359 -9
  69. package/mapping/elementor.json +581 -8
  70. package/mapping/gutenberg.json +420 -4
  71. package/package.json +2 -2
@@ -5,13 +5,15 @@
5
5
  import { tv } from 'tailwind-variants'
6
6
  import KitImage from '../_shared/KitImage.astro'
7
7
  import Section from '../_shared/Section.astro'
8
- import type { ImageInput } from '../_shared/types'
8
+ import { imageOf, type ImageSource } from '../_shared/image'
9
9
 
10
10
  export interface TestimonialItem {
11
11
  quote: string
12
12
  name: string
13
13
  role?: string | undefined
14
- avatar?: ImageInput | undefined
14
+ /** The portrait's address (or, until 0.7, an `ImageInput`). */
15
+ avatar?: ImageSource | undefined
16
+ avatarAlt?: string | undefined
15
17
  }
16
18
 
17
19
  const testimonial = tv({
@@ -57,7 +59,7 @@ const headingId = heading && id ? `${id}-title` : undefined
57
59
  <figure class={s.card()}>
58
60
  <blockquote class={s.quote()}><p>“{item.quote}”</p></blockquote>
59
61
  <figcaption class={s.person()}>
60
- {item.avatar && <KitImage image={item.avatar} sizes="48px" class="size-12 shrink-0 rounded-full object-cover" />}
62
+ {imageOf(item.avatar, item.avatarAlt) && <KitImage image={imageOf(item.avatar, item.avatarAlt)!} sizes="48px" class="size-12 shrink-0 rounded-full object-cover" />}
61
63
  <span class="flex flex-col text-left">
62
64
  <span class={s.name()}>{item.name}</span>
63
65
  {item.role && <span class={s.role()}>{item.role}</span>}
@@ -7,6 +7,7 @@
7
7
  "shared": [
8
8
  "KitImage.astro",
9
9
  "Section.astro",
10
+ "image.ts",
10
11
  "types.ts"
11
12
  ],
12
13
  "variants": {
@@ -32,39 +33,37 @@
32
33
  "props": {
33
34
  "items": {
34
35
  "type": "array",
36
+ "label": "Testimonials",
35
37
  "required": true,
36
38
  "items": {
37
39
  "type": "object",
38
40
  "fields": {
39
41
  "quote": {
40
42
  "type": "text",
41
- "required": true
43
+ "label": "Quote",
44
+ "required": true,
45
+ "description": "What the person said, in their words."
42
46
  },
43
47
  "name": {
44
48
  "type": "string",
45
- "required": true
49
+ "label": "Name",
50
+ "required": true,
51
+ "description": "Who said it."
46
52
  },
47
53
  "role": {
48
- "type": "string"
54
+ "type": "string",
55
+ "label": "Role",
56
+ "description": "Their role or company."
49
57
  },
50
58
  "avatar": {
51
- "type": "object",
52
- "fields": {
53
- "src": {
54
- "type": "image",
55
- "required": true
56
- },
57
- "alt": {
58
- "type": "string",
59
- "required": true
60
- },
61
- "width": {
62
- "type": "integer"
63
- },
64
- "height": {
65
- "type": "integer"
66
- }
67
- }
59
+ "type": "image",
60
+ "label": "Photo",
61
+ "description": "Their photo."
62
+ },
63
+ "avatarAlt": {
64
+ "type": "string",
65
+ "label": "Photo description",
66
+ "description": "Who the photo shows; usually their name."
68
67
  }
69
68
  }
70
69
  },
@@ -72,10 +71,12 @@
72
71
  },
73
72
  "heading": {
74
73
  "type": "string",
74
+ "label": "Heading",
75
75
  "description": "Section heading above the quotes."
76
76
  },
77
77
  "id": {
78
78
  "type": "string",
79
+ "content": false,
79
80
  "description": "Anchor id of the section; the heading gets `<id>-title`."
80
81
  }
81
82
  },
package/dist/index.d.mts CHANGED
@@ -18,7 +18,26 @@ type KitCategory = 'chrome' | 'section' | 'content' | 'primitive';
18
18
  type KitJs = 'none' | 'vanilla' | 'embla' | 'zag' | 'studio';
19
19
  type KitProp = FieldDef & {
20
20
  description: string;
21
+ /**
22
+ * `false`: a setting the plan or writer passes (heading level, a label from `ui:`, an anchor id), not page
23
+ * content. A section's page-singleton field holds only the other props, its content props.
24
+ */
25
+ content?: false;
21
26
  };
27
+ /**
28
+ * A content prop's name: camelCase, a capital only to start a word (`imageAlt`, not `imageURL`), so it
29
+ * turns into exactly one Contentrain field name.
30
+ */
31
+ declare const CONTENT_PROP_NAME: RegExp;
32
+ /**
33
+ * The page-singleton field a content prop is stored in (DECISIONS §7b): the prop name in snake_case, as
34
+ * Contentrain field names are (`ctaLabel` → `cta_label`). The view passes each prop from its field.
35
+ */
36
+ declare const contentFieldName: (prop: string) => string;
37
+ /** The props a section's page-singleton field holds: all but the settings. */
38
+ declare function contentProps(component: KitComponentMeta): Record<string, KitProp>;
39
+ /** A content prop path as its page-singleton field path: every name in it through `contentFieldName`. */
40
+ declare function contentFieldPath(path: string): string;
22
41
  interface KitVariant {
23
42
  options: string[];
24
43
  default: string;
@@ -45,6 +64,19 @@ interface KitComponentMeta {
45
64
  sources: Partial<Record<KitBuilder, string[]>>;
46
65
  /** How it meets its accessibility pattern. */
47
66
  a11y: string;
67
+ /**
68
+ * Sections only: the section's shape in a few words and the signals that show it in a source page, a
69
+ * closed-set label for the classifier and the migration architect (`image beside heading + text`).
70
+ */
71
+ pattern?: {
72
+ summary: string;
73
+ signals: string[];
74
+ };
75
+ /**
76
+ * Sections only, written by the catalog build: each content prop path (`slides[].ctaLabel`) → the
77
+ * page-singleton field path that holds it (`slides[].cta_label`).
78
+ */
79
+ contentFields?: Record<string, string>;
48
80
  }
49
81
  /** A catalog entry: the meta plus the files that make the component. */
50
82
  interface KitComponent extends KitComponentMeta {
@@ -103,10 +135,91 @@ interface MappingTable {
103
135
  */
104
136
  defaults?: Record<string, Record<string, string | number | boolean>>;
105
137
  rules: MappingRule[];
138
+ /**
139
+ * Section rules: what a whole section of a page (an Elementor container, a Gutenberg group or columns,
140
+ * a Divi section — facts `sections[]`) becomes, tried top to bottom before any element rule. The first
141
+ * that matches wins; a section none matches falls through to the element `rules` inside it.
142
+ */
143
+ sections?: SectionRule[];
144
+ }
145
+ /**
146
+ * A named group of a section's leaves: every leaf named `match` (and carrying `attr`) up to `max` of them.
147
+ * `attr` values may list alternatives with `|` (`{ header_size: 'h1|h2' }`).
148
+ */
149
+ interface SectionSlot {
150
+ match: string | string[];
151
+ attr?: Record<string, string | number | boolean>;
152
+ /** At least this many leaves (default 0). */
153
+ min?: number;
154
+ /** At most this many (default unbounded). */
155
+ max?: number;
156
+ }
157
+ /**
158
+ * One section pattern. Values are the table's expressions, prefixed `@<slot> ` to read a slot's leaf
159
+ * (`@title attr:title`); `a || b` takes the first non-empty (builder data first, rendered markup as fallback).
160
+ * A slot of several leaves joins their text into a markdown prop; any other prop reads its first leaf.
161
+ * A value without a slot reads the section's own root (a cover's background image, a container's setting).
162
+ */
163
+ interface SectionRule {
164
+ /** Stable id (`hero.split`, `card-grid.icon-box`): plans record `<builder>:section:<id>`; JEW chooses among them. */
165
+ id: string;
166
+ component: string;
167
+ variant?: Record<string, string>;
168
+ when?: {
169
+ /** Where on the page: `first` is the page's first section, `last` its last. */position?: 'first' | 'last' | 'any'; /** Column count(s) the section must have. */
170
+ columns?: number | number[]; /** Every leaf must fall into a slot (default true); false lets unclaimed leaves be. */
171
+ only?: boolean; /** `columns`: each column must match the slots on its own (a row of team members, price plans). */
172
+ repeat?: 'columns'; /** The section's root element(s): `core/cover`, `core/media-text` (a loose run of blocks is `run`). */
173
+ root?: string | string[];
174
+ };
175
+ /**
176
+ * Named leaf groups. Each leaf, in document order, goes to the first declared slot that matches it and
177
+ * still has room, so declare a specific slot (an `attr` filter) before a general one on the same element.
178
+ */
179
+ slots: Record<string, SectionSlot>;
180
+ props?: Record<string, string>;
181
+ /**
182
+ * Array prop filled once per `each`: a slot's leaves (`@card`), the parts a selector finds inside them
183
+ * (`@list figure.wp-block-image`, `@faq .elementor-accordion-item`: a gallery's images, an accordion's
184
+ * items), or the columns (`column`, with `repeat: 'columns'`).
185
+ */
186
+ into?: string;
187
+ each?: string;
188
+ /** One array item: relative to the leaf (`each: '@slot'`) or to the column, where `@<slot>` is that column's slot. */
189
+ item?: Record<string, string>;
190
+ }
191
+ /** A section leaf as the fact pack gives it: a builder element with its `blocks:` path. */
192
+ interface SectionLeaf extends MappingNode {
193
+ path: string;
106
194
  }
195
+ /** Where a section sits (facts `index`/`count`) and what it is (`root`: facts `root`). */
196
+ interface SectionMeta {
197
+ index: number;
198
+ count: number;
199
+ root?: string | undefined;
200
+ }
201
+ /** Leaf paths per slot; with `repeat: 'columns'`, also per column. */
202
+ interface SectionMatch {
203
+ slots: Record<string, string[]>;
204
+ columns?: Record<string, string[]>[];
205
+ }
206
+ /** The id a plan records for a section rule. */
207
+ declare function sectionRuleId(builder: KitBuilder, rule: Pick<SectionRule, 'id'>): string;
208
+ /**
209
+ * Whether a section rule fits a section, and which leaves fill which slot. `columns` are the section's
210
+ * columns left to right, each its leaves in document order (layout containers already opened).
211
+ */
212
+ declare function matchSection(rule: SectionRule, columns: SectionLeaf[][], meta: SectionMeta, defaults?: MappingTable['defaults']): SectionMatch | null;
213
+ /** The first section rule of a table that fits, with its match. */
214
+ declare function classifySection(table: MappingTable, columns: SectionLeaf[][], meta: SectionMeta): {
215
+ rule: SectionRule;
216
+ match: SectionMatch;
217
+ } | null;
218
+ /** A section value: alternatives joined by ` || `, each an optional `@<slot> ` then a mapping value. Returns the slots it reads, or null when malformed. */
219
+ declare function sectionValueSlots(value: string): string[] | null;
107
220
  /** Problems with a mapping table against the catalog, one line each; empty when it is sound. */
108
221
  declare function validateMapping(table: MappingTable, catalog: KitCatalog): string[];
109
- /** Catalog sources of a builder that no rule of its table matches. */
222
+ /** Catalog sources of a builder that no rule of its table matches, as an element rule or a section slot. */
110
223
  declare function unmappedSources(table: MappingTable, catalog: KitCatalog): string[];
111
224
  /** Rules for an element, most specific (with `when` / qualifier) first. */
112
225
  declare function rulesFor(table: MappingTable, element: string): MappingRule[];
@@ -153,5 +266,5 @@ declare function loadMapping(builder: KitBuilder): Promise<MappingTable>;
153
266
  /** The published catalog (`catalog.json`). */
154
267
  declare function loadCatalog(): Promise<KitCatalog>;
155
268
  //#endregion
156
- export { BUILDER_PREFIX, CATALOG_FORMAT, CopyPlan, KIT_BUILDERS, KIT_COMPONENTS_DIR, KIT_DIR, KitBuilder, KitCatalog, KitCategory, KitComponent, KitComponentMeta, KitJs, KitProp, KitVariant, MAPPING_FORMAT, MAPPING_VALUE_PATTERN, MappingNode, MappingRule, MappingTable, attrsOf, claimMatches, componentsForSource, copyComponents, isBuilderElement, loadCatalog, loadMapping, planCopy, ruleMatches, rulesFor, unmappedSources, validateCatalog, validateMapping };
269
+ export { BUILDER_PREFIX, CATALOG_FORMAT, CONTENT_PROP_NAME, CopyPlan, KIT_BUILDERS, KIT_COMPONENTS_DIR, KIT_DIR, KitBuilder, KitCatalog, KitCategory, KitComponent, KitComponentMeta, KitJs, KitProp, KitVariant, MAPPING_FORMAT, MAPPING_VALUE_PATTERN, MappingNode, MappingRule, MappingTable, SectionLeaf, SectionMatch, SectionMeta, SectionRule, SectionSlot, attrsOf, claimMatches, classifySection, componentsForSource, contentFieldName, contentFieldPath, contentProps, copyComponents, isBuilderElement, loadCatalog, loadMapping, matchSection, planCopy, ruleMatches, rulesFor, sectionRuleId, sectionValueSlots, unmappedSources, validateCatalog, validateMapping };
157
270
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/catalog.ts","../src/mapping.ts","../src/copy.ts","../src/index.ts"],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/catalog.ts","../src/mapping.ts","../src/copy.ts","../src/index.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { copyFile, mkdir, readFile } from "node:fs/promises";
2
2
  import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { fieldDepth, sectionFieldName } from "@contentrain/types";
4
5
  //#region src/catalog.ts
5
6
  const CATALOG_FORMAT = "astro-kit-catalog@1";
6
7
  const KIT_BUILDERS = [
@@ -25,6 +26,33 @@ function isBuilderElement(builder, name) {
25
26
  if (builder !== "gutenberg") return name.startsWith(BUILDER_PREFIX[builder]);
26
27
  return /^[a-z0-9-]+\/[a-z0-9-]+(:[\w-]+)?$/.test(name) && !KIT_BUILDERS.some((b) => b !== "gutenberg" && name.startsWith(BUILDER_PREFIX[b]));
27
28
  }
29
+ /**
30
+ * A content prop's name: camelCase, a capital only to start a word (`imageAlt`, not `imageURL`), so it
31
+ * turns into exactly one Contentrain field name.
32
+ */
33
+ const CONTENT_PROP_NAME = /^[a-z][a-z0-9]*(?:[A-Z][a-z0-9]+)*$/;
34
+ /**
35
+ * The page-singleton field a content prop is stored in (DECISIONS §7b): the prop name in snake_case, as
36
+ * Contentrain field names are (`ctaLabel` → `cta_label`). The view passes each prop from its field.
37
+ */
38
+ const contentFieldName = sectionFieldName;
39
+ /** The props a section's page-singleton field holds: all but the settings. */
40
+ function contentProps(component) {
41
+ return Object.fromEntries(Object.entries(component.props).filter(([, prop]) => prop.content !== false));
42
+ }
43
+ /** Fields under a field, by dotted path (`items[].imageAlt`). */
44
+ function fieldsByPath(fields, prefix = "") {
45
+ const out = [];
46
+ for (const [name, field] of Object.entries(fields)) {
47
+ out.push([prefix + name, field], ...fieldsByPath(field.fields ?? {}, `${prefix}${name}.`));
48
+ if (typeof field.items === "object") out.push(...fieldsByPath(field.items.fields ?? {}, `${prefix}${name}[].`));
49
+ }
50
+ return out;
51
+ }
52
+ /** A content prop path as its page-singleton field path: every name in it through `contentFieldName`. */
53
+ function contentFieldPath(path) {
54
+ return path.replace(/[A-Za-z0-9]+/g, contentFieldName);
55
+ }
28
56
  /** Problems with a catalog, one line each; empty when it is sound. */
29
57
  function validateCatalog(catalog) {
30
58
  const problems = [];
@@ -39,6 +67,20 @@ function validateCatalog(catalog) {
39
67
  if (!prop.description) problems.push(`${at}: prop ${name} has no description`);
40
68
  if (prop.type === "select" && !prop.options?.length) problems.push(`${at}: select prop ${name} has no options`);
41
69
  }
70
+ if (c.category === "section") {
71
+ const content = contentProps(c);
72
+ const paths = fieldsByPath(content);
73
+ for (const [path, field] of paths) {
74
+ if (!CONTENT_PROP_NAME.test(path.split(/\.|\[\]\./).at(-1))) problems.push(`${at}: content prop ${path} must be camelCase with one capital per word (or content: false)`);
75
+ if (!field.label) problems.push(`${at}: content prop ${path} has no label`);
76
+ if (!field.description) problems.push(`${at}: content prop ${path} has no description`);
77
+ }
78
+ if (c.contentFields) {
79
+ const expected = Object.fromEntries(paths.map(([path]) => [path, contentFieldPath(path)]));
80
+ if (JSON.stringify(c.contentFields) !== JSON.stringify(expected)) problems.push(`${at}: contentFields is stale — rebuild the catalog`);
81
+ }
82
+ for (const [name, prop] of Object.entries(content)) if (1 + fieldDepth(prop) > 2) problems.push(`${at}: content prop ${name} nests objects deeper than a page singleton allows`);
83
+ }
42
84
  for (const [builder, names] of Object.entries(c.sources)) {
43
85
  const prefix = BUILDER_PREFIX[builder];
44
86
  for (const name of names) if (!isBuilderElement(builder, name)) problems.push(`${at}: source "${name}" is not a ${builder} element (${prefix}…)`);
@@ -96,6 +138,77 @@ async function copyComponents(plan, kitRoot, siteRoot) {
96
138
  //#region src/mapping.ts
97
139
  const MAPPING_FORMAT = "astro-kit-mapping@1";
98
140
  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.-]+|class:[\w*-]+=(?:[a-z][\w-]*)?\|(?:[a-z][\w-]*)?|(?:dom|img|link|html):[^@]*(?:@[\w-]+)?)$/;
141
+ /** The id a plan records for a section rule. */
142
+ function sectionRuleId(builder, rule) {
143
+ return `${builder}:section:${rule.id}`;
144
+ }
145
+ const SECTION_LAYOUT_LEAVES = new Set([
146
+ "elementor/spacer",
147
+ "elementor/divider",
148
+ "core/spacer",
149
+ "core/separator",
150
+ "divi/et_pb_divider"
151
+ ]);
152
+ function slotFits(slot, leaf, defaults) {
153
+ if (![slot.match].flat().includes(leaf.name)) return false;
154
+ const attrs = attrsOf(leaf, defaults);
155
+ return Object.entries(slot.attr ?? {}).every(([key, want]) => String(want).split("|").includes(String(attrs[key])));
156
+ }
157
+ /** Assign leaves to slots in document order, each to the first declared slot with room; null when the rule does not fit. */
158
+ function claimLeaves(rule, leaves, defaults) {
159
+ const claimed = Object.fromEntries(Object.keys(rule.slots).map((name) => [name, []]));
160
+ for (const leaf of leaves) {
161
+ if (SECTION_LAYOUT_LEAVES.has(leaf.name)) continue;
162
+ const slot = Object.entries(rule.slots).find(([name, def]) => claimed[name].length < (def.max ?? Infinity) && slotFits(def, leaf, defaults));
163
+ if (slot) claimed[slot[0]].push(leaf.path);
164
+ else if (rule.when?.only !== false) return null;
165
+ }
166
+ for (const [name, def] of Object.entries(rule.slots)) if (claimed[name].length < (def.min ?? 0)) return null;
167
+ return claimed;
168
+ }
169
+ /**
170
+ * Whether a section rule fits a section, and which leaves fill which slot. `columns` are the section's
171
+ * columns left to right, each its leaves in document order (layout containers already opened).
172
+ */
173
+ function matchSection(rule, columns, meta, defaults) {
174
+ const when = rule.when ?? {};
175
+ if (when.root !== void 0 && !(meta.root !== void 0 && [when.root].flat().includes(meta.root))) return null;
176
+ if (when.position === "first" && meta.index !== 0) return null;
177
+ if (when.position === "last" && meta.index !== meta.count - 1) return null;
178
+ if (when.columns !== void 0 && ![when.columns].flat().includes(columns.length)) return null;
179
+ if (when.repeat === "columns") {
180
+ if (columns.length < 2) return null;
181
+ const per = columns.map((leaves) => claimLeaves(rule, leaves, defaults));
182
+ if (per.some((c) => !c)) return null;
183
+ return {
184
+ slots: Object.fromEntries(Object.keys(rule.slots).map((name) => [name, per.flatMap((c) => c[name])])),
185
+ columns: per
186
+ };
187
+ }
188
+ const slots = claimLeaves(rule, columns.flat(), defaults);
189
+ return slots && { slots };
190
+ }
191
+ /** The first section rule of a table that fits, with its match. */
192
+ function classifySection(table, columns, meta) {
193
+ for (const rule of table.sections ?? []) {
194
+ const match = matchSection(rule, columns, meta, table.defaults);
195
+ if (match) return {
196
+ rule,
197
+ match
198
+ };
199
+ }
200
+ return null;
201
+ }
202
+ /** A section value: alternatives joined by ` || `, each an optional `@<slot> ` then a mapping value. Returns the slots it reads, or null when malformed. */
203
+ function sectionValueSlots(value) {
204
+ const slots = [];
205
+ for (const alternative of value.split(" || ")) {
206
+ const m = /^(?:@([a-z][\w-]*) )?(.+)$/.exec(alternative);
207
+ if (!m || !MAPPING_VALUE_PATTERN.test(m[2])) return null;
208
+ if (m[1]) slots.push(m[1]);
209
+ }
210
+ return slots;
211
+ }
99
212
  /** Problems with a mapping table against the catalog, one line each; empty when it is sound. */
100
213
  function validateMapping(table, catalog) {
101
214
  const problems = [];
@@ -143,11 +256,70 @@ function validateMapping(table, catalog) {
143
256
  const bound = new Set([...Object.keys(rule.props ?? {}), ...rule.into ? [rule.into] : Object.keys(rule.item ?? {})]);
144
257
  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`);
145
258
  }
259
+ problems.push(...validateSections(table, components));
260
+ return problems;
261
+ }
262
+ function validateSections(table, components) {
263
+ const problems = [];
264
+ const seen = /* @__PURE__ */ new Set();
265
+ for (const rule of table.sections ?? []) {
266
+ const at = `${table.builder} section ${rule.id}`;
267
+ if (!/^[a-z][a-z0-9-]*(?:\.[a-z0-9-]+)*$/.test(rule.id)) problems.push(`${at}: id is not dotted kebab-case`);
268
+ if (seen.has(rule.id)) problems.push(`${at}: listed twice`);
269
+ seen.add(rule.id);
270
+ const c = components.get(rule.component);
271
+ if (!c) {
272
+ problems.push(`${at}: component ${rule.component} is not in the catalog`);
273
+ continue;
274
+ }
275
+ for (const [axis, value] of Object.entries(rule.variant ?? {})) if (!c.variants[axis]) problems.push(`${at}: ${c.id} has no variant axis ${axis}`);
276
+ else if (!c.variants[axis].options.includes(value)) problems.push(`${at}: ${c.id}.${axis} has no option ${value}`);
277
+ if (!Object.keys(rule.slots).length) problems.push(`${at}: no slots`);
278
+ for (const [name, slot] of Object.entries(rule.slots)) {
279
+ if (!/^[a-z][\w-]*$/.test(name)) problems.push(`${at}: slot ${name} is not an identifier`);
280
+ for (const element of [slot.match].flat()) if (!isBuilderElement(table.builder, element)) problems.push(`${at}: slot ${name} matches ${element}, not a ${table.builder} element`);
281
+ if (slot.min !== void 0 && slot.max !== void 0 && slot.min > slot.max) problems.push(`${at}: slot ${name} min > max`);
282
+ }
283
+ for (const root of [rule.when?.root ?? []].flat()) if (root !== "run" && !isBuilderElement(table.builder, root)) problems.push(`${at}: root ${root} is not a ${table.builder} element or run`);
284
+ if (rule.when?.repeat === "columns" && rule.each !== "column") problems.push(`${at}: repeat columns needs each: column`);
285
+ if (rule.each === "column" && rule.when?.repeat !== "columns") problems.push(`${at}: each column needs when.repeat columns`);
286
+ const readsSlots = (value, where, type, slotted) => {
287
+ const slots = sectionValueSlots(value);
288
+ if (!slots) {
289
+ problems.push(`${at}: ${where} = ${value} is not a section value`);
290
+ return;
291
+ }
292
+ if (!slotted && slots.length) problems.push(`${at}: ${where} reads a slot, but items of each ${rule.each} read their own leaf`);
293
+ for (const slot of slots) {
294
+ const def = rule.slots[slot];
295
+ if (!def) problems.push(`${at}: ${where} reads undeclared slot ${slot}`);
296
+ else if ((type === "string" || type === "text") && def.max !== 1) problems.push(`${at}: ${where} is ${type} but slot ${slot} may hold several leaves (max must be 1)`);
297
+ }
298
+ };
299
+ for (const [prop, value] of Object.entries(rule.props ?? {})) {
300
+ const def = c.props[prop];
301
+ if (!def) problems.push(`${at}: ${c.id} has no prop ${prop}`);
302
+ readsSlots(value, prop, def?.type, true);
303
+ }
304
+ if (rule.into) {
305
+ const target = c.props[rule.into];
306
+ if (!target || target.type !== "array") problems.push(`${at}: ${c.id}.${rule.into} is not an array prop`);
307
+ if (!rule.each) problems.push(`${at}: into ${rule.into} needs each`);
308
+ else if (rule.each !== "column" && !rule.slots[/^@([a-z][\w-]*)(?: \S.*)?$/.exec(rule.each)?.[1] ?? ""]) problems.push(`${at}: each ${rule.each} is not @<declared slot> [selector] or column`);
309
+ const fields = target && typeof target.items === "object" ? target.items.fields ?? {} : {};
310
+ for (const [field, value] of Object.entries(rule.item ?? {})) {
311
+ if (!fields[field]) problems.push(`${at}: ${c.id}.${rule.into}[] has no field ${field}`);
312
+ readsSlots(value, `${rule.into}[].${field}`, fields[field]?.type, rule.each === "column");
313
+ }
314
+ } else if (rule.item || rule.each) problems.push(`${at}: each/item without into`);
315
+ const bound = new Set([...Object.keys(rule.props ?? {}), ...rule.into ? [rule.into] : []]);
316
+ 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`);
317
+ }
146
318
  return problems;
147
319
  }
148
- /** Catalog sources of a builder that no rule of its table matches. */
320
+ /** Catalog sources of a builder that no rule of its table matches, as an element rule or a section slot. */
149
321
  function unmappedSources(table, catalog) {
150
- const matched = new Set(table.rules.map((r) => r.match));
322
+ const matched = new Set([...table.rules.map((r) => r.match), ...(table.sections ?? []).flatMap((r) => Object.values(r.slots).flatMap((s) => [s.match].flat()))]);
151
323
  const sources = catalog.components.flatMap((c) => c.sources[table.builder] ?? []);
152
324
  return [...new Set(sources)].filter((s) => !matched.has(s)).toSorted();
153
325
  }
@@ -206,6 +378,6 @@ async function loadCatalog() {
206
378
  return JSON.parse(await readFile(join(PACKAGE_ROOT, "catalog.json"), "utf8"));
207
379
  }
208
380
  //#endregion
209
- export { BUILDER_PREFIX, CATALOG_FORMAT, KIT_BUILDERS, KIT_COMPONENTS_DIR, KIT_DIR, MAPPING_FORMAT, MAPPING_VALUE_PATTERN, attrsOf, claimMatches, componentsForSource, copyComponents, isBuilderElement, loadCatalog, loadMapping, planCopy, ruleMatches, rulesFor, unmappedSources, validateCatalog, validateMapping };
381
+ export { BUILDER_PREFIX, CATALOG_FORMAT, CONTENT_PROP_NAME, KIT_BUILDERS, KIT_COMPONENTS_DIR, KIT_DIR, MAPPING_FORMAT, MAPPING_VALUE_PATTERN, attrsOf, claimMatches, classifySection, componentsForSource, contentFieldName, contentFieldPath, contentProps, copyComponents, isBuilderElement, loadCatalog, loadMapping, matchSection, planCopy, ruleMatches, rulesFor, sectionRuleId, sectionValueSlots, unmappedSources, validateCatalog, validateMapping };
210
382
 
211
383
  //# sourceMappingURL=index.mjs.map