@refrakt-md/runes 0.42.0 → 0.44.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.
- package/dist/composed-rune.d.ts +9 -1
- package/dist/composed-rune.d.ts.map +1 -1
- package/dist/composed-rune.js +14 -2
- package/dist/composed-rune.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +27 -1
- package/dist/config.js.map +1 -1
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +2 -0
- package/dist/examples.js.map +1 -1
- package/dist/index.d.ts +5 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -8
- package/dist/index.js.map +1 -1
- package/dist/lib/component.d.ts +5 -1
- package/dist/lib/component.d.ts.map +1 -1
- package/dist/lib/component.js +0 -1
- package/dist/lib/component.js.map +1 -1
- package/dist/lib/composition-errors.d.ts +7 -0
- package/dist/lib/composition-errors.d.ts.map +1 -1
- package/dist/lib/composition-errors.js +7 -0
- package/dist/lib/composition-errors.js.map +1 -1
- package/dist/lib/composition-placement.d.ts +88 -0
- package/dist/lib/composition-placement.d.ts.map +1 -0
- package/dist/lib/composition-placement.js +180 -0
- package/dist/lib/composition-placement.js.map +1 -0
- package/dist/lib/composition.d.ts +60 -3
- package/dist/lib/composition.d.ts.map +1 -1
- package/dist/lib/composition.js +263 -38
- package/dist/lib/composition.js.map +1 -1
- package/dist/lib/header-roles.d.ts +32 -0
- package/dist/lib/header-roles.d.ts.map +1 -0
- package/dist/lib/header-roles.js +52 -0
- package/dist/lib/header-roles.js.map +1 -0
- package/dist/lib/index.d.ts +2 -2
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +161 -243
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/resolver.d.ts +5 -0
- package/dist/lib/resolver.d.ts.map +1 -1
- package/dist/lib/resolver.js +25 -3
- package/dist/lib/resolver.js.map +1 -1
- package/dist/lib/slot-markers.js +9 -1
- package/dist/lib/slot-markers.js.map +1 -1
- package/dist/lib/slots.d.ts +5 -2
- package/dist/lib/slots.d.ts.map +1 -1
- package/dist/lib/slots.js +1 -1
- package/dist/lib/slots.js.map +1 -1
- package/dist/lib/universal-attribute-defs.d.ts +12 -0
- package/dist/lib/universal-attribute-defs.d.ts.map +1 -0
- package/dist/lib/universal-attribute-defs.js +225 -0
- package/dist/lib/universal-attribute-defs.js.map +1 -0
- package/dist/nodes.d.ts +11 -0
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +34 -0
- package/dist/nodes.js.map +1 -1
- package/dist/plugins.d.ts.map +1 -1
- package/dist/plugins.js +36 -1
- package/dist/plugins.js.map +1 -1
- package/dist/project-runes.d.ts +9 -2
- package/dist/project-runes.d.ts.map +1 -1
- package/dist/project-runes.js +5 -5
- package/dist/project-runes.js.map +1 -1
- package/dist/reference.d.ts.map +1 -1
- package/dist/reference.js +9 -0
- package/dist/reference.js.map +1 -1
- package/dist/tags/accordion.d.ts +1 -0
- package/dist/tags/accordion.d.ts.map +1 -1
- package/dist/tags/accordion.js +1 -1
- package/dist/tags/accordion.js.map +1 -1
- package/dist/tags/actions.d.ts +20 -0
- package/dist/tags/actions.d.ts.map +1 -0
- package/dist/tags/actions.js +72 -0
- package/dist/tags/actions.js.map +1 -0
- package/dist/tags/blog.d.ts.map +1 -1
- package/dist/tags/blog.js +0 -1
- package/dist/tags/blog.js.map +1 -1
- package/dist/tags/budget.d.ts.map +1 -1
- package/dist/tags/budget.js +0 -1
- package/dist/tags/budget.js.map +1 -1
- package/dist/tags/card.d.ts +3 -1
- package/dist/tags/card.d.ts.map +1 -1
- package/dist/tags/card.js +7 -4
- package/dist/tags/card.js.map +1 -1
- package/dist/tags/common.d.ts +9 -0
- package/dist/tags/common.d.ts.map +1 -1
- package/dist/tags/common.js +11 -0
- package/dist/tags/common.js.map +1 -1
- package/dist/tags/hint.d.ts.map +1 -1
- package/dist/tags/hint.js +0 -1
- package/dist/tags/hint.js.map +1 -1
- package/dist/tags/reveal.d.ts +1 -0
- package/dist/tags/reveal.d.ts.map +1 -1
- package/dist/tags/reveal.js +1 -1
- package/dist/tags/reveal.js.map +1 -1
- package/dist/tags/section.d.ts +43 -2
- package/dist/tags/section.d.ts.map +1 -1
- package/dist/tags/section.js +178 -10
- package/dist/tags/section.js.map +1 -1
- package/dist/tags/tabs.d.ts +1 -0
- package/dist/tags/tabs.d.ts.map +1 -1
- package/dist/tags/tabs.js +1 -1
- package/dist/tags/tabs.js.map +1 -1
- package/package.json +3 -3
- package/dist/documents/doc.d.ts +0 -4
- package/dist/documents/doc.d.ts.map +0 -1
- package/dist/documents/doc.js +0 -4
- package/dist/documents/doc.js.map +0 -1
- package/dist/documents/page.d.ts +0 -10
- package/dist/documents/page.d.ts.map +0 -1
- package/dist/documents/page.js +0 -30
- package/dist/documents/page.js.map +0 -1
package/dist/lib/composition.js
CHANGED
|
@@ -68,6 +68,8 @@ import { SchemaSideTable } from './schema-side-table.js';
|
|
|
68
68
|
import { declareSlotMarkers, declareSlotMarkersOnNodes } from './slot-markers.js';
|
|
69
69
|
import { registersSources } from './registers.js';
|
|
70
70
|
import { parse } from '../parse.js';
|
|
71
|
+
import { universalAttributeDefinition } from './universal-attribute-defs.js';
|
|
72
|
+
import { moveAttributes, resolvePlacements } from './composition-placement.js';
|
|
71
73
|
const { Ast, Tag: MarkdocTag } = Markdoc;
|
|
72
74
|
/** Compositions by schema — the same pattern as `schemaTables`: tooling has only
|
|
73
75
|
* the schema to read from. */
|
|
@@ -212,6 +214,8 @@ function slotFields(model, fail) {
|
|
|
212
214
|
refs,
|
|
213
215
|
...(listItems ? { listItems } : {}),
|
|
214
216
|
...(ambiguousList ? { ambiguousList } : {}),
|
|
217
|
+
...(f.fills ? { fills: f.fills } : {}),
|
|
218
|
+
...(f.itemModel ? { itemFields: f.itemModel.fields.map((i) => i.name) } : {}),
|
|
215
219
|
});
|
|
216
220
|
};
|
|
217
221
|
if (s.type === 'sequence') {
|
|
@@ -259,47 +263,56 @@ function slotValues(model, resolved) {
|
|
|
259
263
|
// Template parsing and the construction-time checks
|
|
260
264
|
// ---------------------------------------------------------------------------
|
|
261
265
|
/**
|
|
262
|
-
* `{% slot name="x" each %}`
|
|
263
|
-
*
|
|
264
|
-
*
|
|
266
|
+
* `{% slot name="x" each %}` and `{% section chrome %}` are the spec's
|
|
267
|
+
* spellings (D26, D31), and Markdoc's tag grammar has no bare boolean
|
|
268
|
+
* attribute. Rewrite `each` inside slot tags, and `chrome` inside any tag, to
|
|
269
|
+
* `=true`, outside quoted values, before parsing.
|
|
265
270
|
*/
|
|
266
271
|
export function normaliseEach(body) {
|
|
267
|
-
return body.replace(/\{%-?\s*
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
i
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
if (ch === '"' || ch === "'") {
|
|
286
|
-
quote = ch;
|
|
287
|
-
out += ch;
|
|
288
|
-
i++;
|
|
272
|
+
return body.replace(/\{%-?\s*([a-z][\w-]*)\b([^%]*?)(-?%\})/g, (whole, tag, inner, close) => {
|
|
273
|
+
const bare = tag === 'slot' ? ['each', 'chrome'] : ['chrome'];
|
|
274
|
+
const out = bareToTrue(inner, bare);
|
|
275
|
+
return out === inner ? whole : whole.replace(inner + close, out + close);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/** Rewrite each bare word in `words` to `word=true`, outside quoted values. */
|
|
279
|
+
function bareToTrue(inner, words) {
|
|
280
|
+
let out = '';
|
|
281
|
+
let quote = null;
|
|
282
|
+
let i = 0;
|
|
283
|
+
while (i < inner.length) {
|
|
284
|
+
const ch = inner[i];
|
|
285
|
+
if (quote) {
|
|
286
|
+
if (ch === '\\') {
|
|
287
|
+
out += inner.slice(i, i + 2);
|
|
288
|
+
i += 2;
|
|
289
289
|
continue;
|
|
290
290
|
}
|
|
291
|
+
if (ch === quote)
|
|
292
|
+
quote = null;
|
|
293
|
+
out += ch;
|
|
294
|
+
i++;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (ch === '"' || ch === "'") {
|
|
298
|
+
quote = ch;
|
|
299
|
+
out += ch;
|
|
300
|
+
i++;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (i === 0 || /\s/.test(inner[i - 1])) {
|
|
291
304
|
const rest = inner.slice(i);
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
out +=
|
|
295
|
-
i +=
|
|
305
|
+
const word = words.find((w) => new RegExp(`^${w}(?![\\w=-])`).test(rest));
|
|
306
|
+
if (word) {
|
|
307
|
+
out += `${word}=true`;
|
|
308
|
+
i += word.length;
|
|
296
309
|
continue;
|
|
297
310
|
}
|
|
298
|
-
out += ch;
|
|
299
|
-
i++;
|
|
300
311
|
}
|
|
301
|
-
|
|
302
|
-
|
|
312
|
+
out += ch;
|
|
313
|
+
i++;
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
303
316
|
}
|
|
304
317
|
/** Variables a template node reads, by path, in attribute or content position. */
|
|
305
318
|
function variablePaths(value, out = []) {
|
|
@@ -370,6 +383,8 @@ export function compileComposition(template, options) {
|
|
|
370
383
|
const placedSlots = new Map();
|
|
371
384
|
const slotOrder = [];
|
|
372
385
|
const placements = [];
|
|
386
|
+
/** Every rune the template places, for the paint carrier and role owners. */
|
|
387
|
+
const templateRunes = [];
|
|
373
388
|
const checkVariables = (n, scope) => {
|
|
374
389
|
for (const path of variablePaths(n.attributes)) {
|
|
375
390
|
const [root, name] = path;
|
|
@@ -438,6 +453,7 @@ export function compileComposition(template, options) {
|
|
|
438
453
|
fail('composition-cycle', `it places \`{% ${n.tag} %}\`, which is the rune itself — a composition cycle.`);
|
|
439
454
|
}
|
|
440
455
|
placements.push({ rune: n.tag, ...(scope.parentRune ? { parent: scope.parentRune } : {}) });
|
|
456
|
+
templateRunes.push({ node: n, top: !scope.parentRune, inEach: !!scope.each });
|
|
441
457
|
const children = outlineOf(n.children, { ...scope, parentRune: n.tag });
|
|
442
458
|
return [{ rune: n.tag, ...(children.length > 0 ? { children } : {}) }];
|
|
443
459
|
}
|
|
@@ -476,6 +492,11 @@ export function compileComposition(template, options) {
|
|
|
476
492
|
if (!field) {
|
|
477
493
|
fail('slot-unknown-field', `slot \`${name}\` names no field of the content model (fields: ${[...fields.keys()].join(', ') || 'none'}) (D26).`);
|
|
478
494
|
}
|
|
495
|
+
if (field?.fills) {
|
|
496
|
+
fail('fill-placed', `slot \`${name}\` places field \`${name}\`, which fills ${Object.keys(field.fills)
|
|
497
|
+
.map((a) => `\`${a}\``)
|
|
498
|
+
.join(', ')}. A filling field is placed into the rune's values, which the template places through a meta block or \`$attrs\`; a slot as well would render it twice. Remove the slot (SPEC-182 D5).`);
|
|
499
|
+
}
|
|
479
500
|
if (placedSlots.has(name)) {
|
|
480
501
|
fail('slot-placed-twice', `slot \`${name}\` is placed twice. A slot is placed exactly once; two positions are two declared fields, or a theme's concern (D26).`);
|
|
481
502
|
}
|
|
@@ -538,8 +559,63 @@ export function compileComposition(template, options) {
|
|
|
538
559
|
return { metablock: name };
|
|
539
560
|
}
|
|
540
561
|
const outline = outlineOf(ast.children, {});
|
|
541
|
-
//
|
|
542
|
-
const
|
|
562
|
+
// D14, D31, D33 — the paint carrier and every axis's target.
|
|
563
|
+
const plan = resolvePlacements(rune, templateRunes, (code, msg, at) => {
|
|
564
|
+
current = at ?? current;
|
|
565
|
+
return fail(code, msg);
|
|
566
|
+
});
|
|
567
|
+
current = undefined;
|
|
568
|
+
// D34 — each default lands where the author's attribute would. One that
|
|
569
|
+
// lands nowhere is a definition error, not a silent no-op.
|
|
570
|
+
const defaultsByNode = new Map();
|
|
571
|
+
const defaultInfo = {};
|
|
572
|
+
for (const [name, value] of Object.entries(template.defaults ?? {})) {
|
|
573
|
+
const target = name === 'density'
|
|
574
|
+
? plan.carrier
|
|
575
|
+
? { placement: 'carrier', node: plan.carrier, rune: plan.carrier.tag }
|
|
576
|
+
: { placement: 'carrier', dropped: `\`${rune}\` places no rune to carry it` }
|
|
577
|
+
: plan.axes[plan.axisOf[name]];
|
|
578
|
+
if (!target)
|
|
579
|
+
continue;
|
|
580
|
+
if (target.placement === 'root') {
|
|
581
|
+
defaultInfo[name] = { value, placement: 'root' };
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (!('node' in target)) {
|
|
585
|
+
const why = 'dropped' in target ? target.dropped : target.unavailable;
|
|
586
|
+
throw new CompositionError('defaults-invalid', `Rune "${rune}": \`defaults.${name}\` lands nowhere: ${why} (D34).`);
|
|
587
|
+
}
|
|
588
|
+
const into = defaultsByNode.get(target.node) ?? {};
|
|
589
|
+
into[name] = value;
|
|
590
|
+
defaultsByNode.set(target.node, into);
|
|
591
|
+
const literal = target.node.attributes[name];
|
|
592
|
+
defaultInfo[name] = {
|
|
593
|
+
value,
|
|
594
|
+
placement: target.placement,
|
|
595
|
+
rune: target.rune,
|
|
596
|
+
...(literal !== undefined && typeof literal !== 'object' ? { literal: String(literal) } : {}),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
// SPEC-182 D2 — `fills` names declared attributes and item-model fields.
|
|
600
|
+
for (const [name, field] of fields) {
|
|
601
|
+
if (!field.fills)
|
|
602
|
+
continue;
|
|
603
|
+
if (!field.itemFields) {
|
|
604
|
+
fail('fills-invalid', `field \`${name}\` declares \`fills\` but no \`itemModel\`: there is nothing to fill from. Give it an item model that reads the values (SPEC-182 D1).`);
|
|
605
|
+
}
|
|
606
|
+
for (const [attr, ref] of Object.entries(field.fills)) {
|
|
607
|
+
if (!declared.has(attr)) {
|
|
608
|
+
fail('fills-invalid', `field \`${name}\` fills \`${attr}\`, which the rune does not declare (declared: ${[...declared].join(', ') || 'none'}). Declare it under \`attributes\` (SPEC-182 D2).`);
|
|
609
|
+
}
|
|
610
|
+
const source = typeof ref === 'string' && ref.startsWith('$') ? ref.slice(1) : undefined;
|
|
611
|
+
if (!source || !field.itemFields.includes(source)) {
|
|
612
|
+
fail('fills-invalid', `field \`${name}\` fills \`${attr}\` from \`${ref}\`, which names no field of its item model (fields: ${field.itemFields.join(', ')}). Write it as \`$<field>\` (SPEC-182 D2).`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
// D11 — a field no slot places parses and then silently does not render. A
|
|
617
|
+
// filling field is placed into the rune's values (SPEC-182 D5).
|
|
618
|
+
const unplaced = [...fields.keys()].filter((f) => !placedSlots.has(f) && !fields.get(f).fills);
|
|
543
619
|
if (unplaced.length > 0) {
|
|
544
620
|
fail('field-unplaced', `content-model field${unplaced.length > 1 ? 's' : ''} ${unplaced.map((f) => `\`${f}\``).join(', ')} ${unplaced.length > 1 ? 'are' : 'is'} placed by no slot, so authored content would be dropped. Place ${unplaced.length > 1 ? 'each' : 'it'} with \`{% slot name="…" /%}\` (D11).`);
|
|
545
621
|
}
|
|
@@ -548,15 +624,26 @@ export function compileComposition(template, options) {
|
|
|
548
624
|
options.schema?.fallback?.type ??
|
|
549
625
|
Object.values(options.schema?.rows ?? {}).find((r) => r.type)?.type;
|
|
550
626
|
const nodeSources = registersNodeSources(options.registers, fields, declared);
|
|
627
|
+
const axes = {};
|
|
628
|
+
for (const [axis, target] of Object.entries(plan.axes)) {
|
|
629
|
+
axes[axis] = {
|
|
630
|
+
placement: target.placement,
|
|
631
|
+
...('rune' in target ? { rune: target.rune } : {}),
|
|
632
|
+
...('dropped' in target ? { dropped: target.dropped } : {}),
|
|
633
|
+
...('unavailable' in target ? { unavailable: target.unavailable } : {}),
|
|
634
|
+
};
|
|
635
|
+
}
|
|
551
636
|
const info = {
|
|
552
637
|
rune,
|
|
553
638
|
slots: slotOrder,
|
|
554
639
|
placements,
|
|
555
640
|
outline,
|
|
641
|
+
axes,
|
|
642
|
+
...(Object.keys(defaultInfo).length > 0 ? { defaults: defaultInfo } : {}),
|
|
556
643
|
...(schemaType ? { schemaType } : {}),
|
|
557
644
|
};
|
|
558
645
|
const attributeNames = [...declared];
|
|
559
|
-
const
|
|
646
|
+
const place = (resolved, attrs) => {
|
|
560
647
|
const bound = {};
|
|
561
648
|
for (const name of attributeNames)
|
|
562
649
|
bound[name] = attrs[name];
|
|
@@ -565,6 +652,8 @@ export function compileComposition(template, options) {
|
|
|
565
652
|
resolved: slotValues(structural, resolved),
|
|
566
653
|
attrs: bound,
|
|
567
654
|
fields,
|
|
655
|
+
moves: moveAttributes(plan, attrs).moves,
|
|
656
|
+
defaults: defaultsByNode,
|
|
568
657
|
};
|
|
569
658
|
const placed = expand(ast.children, ctx);
|
|
570
659
|
// Top-level template nodes are the composed rune's own (D10). A slot
|
|
@@ -573,6 +662,14 @@ export function compileComposition(template, options) {
|
|
|
573
662
|
if (n.attributes[OWNER_ATTR] === undefined)
|
|
574
663
|
n.attributes[OWNER_ATTR] = rune;
|
|
575
664
|
}
|
|
665
|
+
return placed;
|
|
666
|
+
};
|
|
667
|
+
const transform = (resolved, attrs, config, node) => {
|
|
668
|
+
// SPEC-182 D2 — values a paragraph fills become the rune's own, before
|
|
669
|
+
// anything reads them. `attrs` is the caller's object, filled in place, so
|
|
670
|
+
// the schema table applied after this transform reads them too.
|
|
671
|
+
Object.assign(attrs, filledAttributes(resolved, node?.attributes ?? {}));
|
|
672
|
+
const placed = place(resolved, attrs);
|
|
576
673
|
const children = Markdoc.transform(placed, templateConfig(config));
|
|
577
674
|
// The field bag holds every attribute the author set (or that defaulted):
|
|
578
675
|
// it is what `modifiers`, the schema row's attribute sources and
|
|
@@ -593,7 +690,61 @@ export function compileComposition(template, options) {
|
|
|
593
690
|
copyRegisteredNodes(root, nodeSources);
|
|
594
691
|
return root;
|
|
595
692
|
};
|
|
596
|
-
|
|
693
|
+
/** SPEC-182 D2, D4 — what the filling fields supply, for the attributes the
|
|
694
|
+
* author did not write. The author's attribute always wins. */
|
|
695
|
+
const filledAttributes = (resolved, written) => {
|
|
696
|
+
const out = {};
|
|
697
|
+
const values = slotValues(structural, resolved);
|
|
698
|
+
for (const [name, field] of fields) {
|
|
699
|
+
if (!field.fills)
|
|
700
|
+
continue;
|
|
701
|
+
const record = values[`${name}Data`];
|
|
702
|
+
if (!record)
|
|
703
|
+
continue;
|
|
704
|
+
for (const [attr, ref] of Object.entries(field.fills)) {
|
|
705
|
+
if (written[attr] !== undefined)
|
|
706
|
+
continue;
|
|
707
|
+
const value = record[ref.slice(1)];
|
|
708
|
+
if (value !== undefined && value !== '')
|
|
709
|
+
out[attr] = value;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
return out;
|
|
713
|
+
};
|
|
714
|
+
/** SPEC-182 D4 — attributes the author wrote that a paragraph would also have
|
|
715
|
+
* filled: the paragraph is consumed and does not render. */
|
|
716
|
+
const overriddenFills = (resolved, written) => {
|
|
717
|
+
const out = [];
|
|
718
|
+
const values = slotValues(structural, resolved);
|
|
719
|
+
for (const [name, field] of fields) {
|
|
720
|
+
if (!field.fills || !values[`${name}Data`])
|
|
721
|
+
continue;
|
|
722
|
+
for (const attr of Object.keys(field.fills)) {
|
|
723
|
+
if (written[attr] !== undefined)
|
|
724
|
+
out.push({ attribute: attr, field: name });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return out;
|
|
728
|
+
};
|
|
729
|
+
const rootAttributes = (attrs) => {
|
|
730
|
+
const { moved } = moveAttributes(plan, attrs);
|
|
731
|
+
if (moved.size === 0)
|
|
732
|
+
return attrs;
|
|
733
|
+
const out = {};
|
|
734
|
+
for (const [name, value] of Object.entries(attrs))
|
|
735
|
+
if (!moved.has(name))
|
|
736
|
+
out[name] = value;
|
|
737
|
+
return out;
|
|
738
|
+
};
|
|
739
|
+
return {
|
|
740
|
+
transform,
|
|
741
|
+
info,
|
|
742
|
+
place,
|
|
743
|
+
placements: plan,
|
|
744
|
+
rootAttributes,
|
|
745
|
+
overriddenFills,
|
|
746
|
+
templateNodes: ast.children,
|
|
747
|
+
};
|
|
597
748
|
}
|
|
598
749
|
function asNodes(value) {
|
|
599
750
|
if (Array.isArray(value))
|
|
@@ -663,6 +814,19 @@ function substituteNode(node, ctx) {
|
|
|
663
814
|
}
|
|
664
815
|
attributes[key] = resolved;
|
|
665
816
|
}
|
|
817
|
+
if (node.type === 'tag') {
|
|
818
|
+
// D31 — the marker is the template's, consumed here: it reaches neither
|
|
819
|
+
// the placed rune's attributes nor the HTML.
|
|
820
|
+
delete attributes.chrome;
|
|
821
|
+
// D34.3 — author > template literal > definition default.
|
|
822
|
+
for (const [name, value] of Object.entries(ctx.defaults?.get(node) ?? {})) {
|
|
823
|
+
if (attributes[name] === undefined)
|
|
824
|
+
attributes[name] = value;
|
|
825
|
+
}
|
|
826
|
+
const moved = ctx.moves?.get(node);
|
|
827
|
+
if (moved)
|
|
828
|
+
Object.assign(attributes, moved);
|
|
829
|
+
}
|
|
666
830
|
return cloneNode(node, attributes, expand(node.children, ctx));
|
|
667
831
|
}
|
|
668
832
|
/** The items a list field resolved to, each with its `$each` binding and content. */
|
|
@@ -858,6 +1022,7 @@ const DEFINITION_KEYS = new Set([
|
|
|
858
1022
|
'metaFields',
|
|
859
1023
|
'blocks',
|
|
860
1024
|
'provides',
|
|
1025
|
+
'defaults',
|
|
861
1026
|
]);
|
|
862
1027
|
/** Keys that would give a composed rune styles of its own (D2). */
|
|
863
1028
|
const STYLE_KEYS = new Set(['css', 'style', 'styles', 'stylesheet', 'block', 'class']);
|
|
@@ -970,6 +1135,7 @@ export function parseCompositionDefinition(rune, source) {
|
|
|
970
1135
|
(!Array.isArray(fm.provides) || !fm.provides.every((p) => typeof p === 'string'))) {
|
|
971
1136
|
fail('invalid-value', '`provides` must be a list of names.', ['provides']);
|
|
972
1137
|
}
|
|
1138
|
+
const defaults = parseDefaults(fm.defaults, (code, msg, at) => fail(code, msg, ['defaults', ...(at ?? [])]));
|
|
973
1139
|
return {
|
|
974
1140
|
rune,
|
|
975
1141
|
tag,
|
|
@@ -982,12 +1148,46 @@ export function parseCompositionDefinition(rune, source) {
|
|
|
982
1148
|
...(fm.metaFields ? { metaFields: fm.metaFields } : {}),
|
|
983
1149
|
...(fm.blocks ? { blocks: fm.blocks } : {}),
|
|
984
1150
|
...(fm.provides ? { provides: fm.provides } : {}),
|
|
1151
|
+
...(defaults ? { defaults } : {}),
|
|
985
1152
|
template: match[2],
|
|
986
1153
|
// `---`, the frontmatter's lines, `---`: the body starts on the next.
|
|
987
1154
|
templateLine: frontmatter.split(/\r?\n/).length + 3,
|
|
988
1155
|
frontmatter,
|
|
989
1156
|
};
|
|
990
1157
|
}
|
|
1158
|
+
/** D34.6 — `density` has no author-facing attribute, so its scale is stated here. */
|
|
1159
|
+
const DENSITY_VALUES = ['full', 'compact', 'minimal'];
|
|
1160
|
+
/**
|
|
1161
|
+
* D34 — `defaults`: universal axes only, each with a value the axis accepts.
|
|
1162
|
+
* A rune's own attribute takes its default where it is declared.
|
|
1163
|
+
*/
|
|
1164
|
+
function parseDefaults(raw, fail) {
|
|
1165
|
+
if (raw === undefined)
|
|
1166
|
+
return undefined;
|
|
1167
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1168
|
+
fail('defaults-invalid', '`defaults` must be a mapping of universal attributes to values.');
|
|
1169
|
+
}
|
|
1170
|
+
const out = {};
|
|
1171
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
1172
|
+
if (value === null || typeof value === 'object') {
|
|
1173
|
+
fail('defaults-invalid', `\`defaults.${key}\` must be a single value.`, [key]);
|
|
1174
|
+
}
|
|
1175
|
+
const text = String(value);
|
|
1176
|
+
const accepted = key === 'density'
|
|
1177
|
+
? DENSITY_VALUES
|
|
1178
|
+
: universalAttributeDefinition(key) === undefined
|
|
1179
|
+
? undefined
|
|
1180
|
+
: universalAttributeDefinition(key).matches;
|
|
1181
|
+
if (key !== 'density' && universalAttributeDefinition(key) === undefined) {
|
|
1182
|
+
fail('defaults-invalid', `\`defaults\` takes universal attributes only, and \`${key}\` is not one. A rune's own attribute takes its default where it is declared, under \`attributes\` (D34).`, [key]);
|
|
1183
|
+
}
|
|
1184
|
+
if (Array.isArray(accepted) && !accepted.includes(text)) {
|
|
1185
|
+
fail('defaults-invalid', `\`defaults.${key}\` is \`${text}\`, which \`${key}\` does not accept (${accepted.join(', ')}).`, [key]);
|
|
1186
|
+
}
|
|
1187
|
+
out[key] = text;
|
|
1188
|
+
}
|
|
1189
|
+
return out;
|
|
1190
|
+
}
|
|
991
1191
|
function parseAttributes(raw, sectionFail) {
|
|
992
1192
|
const fail = (code, msg) => sectionFail(code, msg);
|
|
993
1193
|
if (raw === undefined)
|
|
@@ -1038,6 +1238,7 @@ const FIELD_KEYS = new Set([
|
|
|
1038
1238
|
'itemModel',
|
|
1039
1239
|
'emitTag',
|
|
1040
1240
|
'emitAttributes',
|
|
1241
|
+
'fills',
|
|
1041
1242
|
]);
|
|
1042
1243
|
/** A field list as YAML writes it: a mapping keyed by name, or a list with `name`. */
|
|
1043
1244
|
function parseFields(raw, where, fail) {
|
|
@@ -1302,6 +1503,13 @@ function parseDelimitedModel(c, fail) {
|
|
|
1302
1503
|
* resolves a field against the rune's modifier values, exactly as it does for a
|
|
1303
1504
|
* tree-owning rune — which must declare the same attributes as modifiers for
|
|
1304
1505
|
* its own blocks to fill.
|
|
1506
|
+
*
|
|
1507
|
+
* An enum attribute the template only forwards to a placed rune
|
|
1508
|
+
* (`{% section media-position=$attrs["media-position"] %}`) is that rune's
|
|
1509
|
+
* input, not a modifier of the composition: as a `data-*` on the empty root it
|
|
1510
|
+
* would match the dimension rules meant for the rune it is forwarded to — a
|
|
1511
|
+
* `data-media-position="cover"` root is laid out as a cover well. It stays in
|
|
1512
|
+
* the field bag, which the schema row and registration read.
|
|
1305
1513
|
*/
|
|
1306
1514
|
export function composedRuneConfig(def) {
|
|
1307
1515
|
const read = new Set();
|
|
@@ -1313,22 +1521,39 @@ export function composedRuneConfig(def) {
|
|
|
1313
1521
|
read.add(ref);
|
|
1314
1522
|
}
|
|
1315
1523
|
}
|
|
1524
|
+
const forwarded = forwardedAttributes(def.template);
|
|
1316
1525
|
const modifiers = {};
|
|
1317
1526
|
for (const [name, attr] of Object.entries(def.attributes)) {
|
|
1318
|
-
if (!
|
|
1527
|
+
if (!read.has(name) && (!attr.matches || forwarded.has(name)))
|
|
1319
1528
|
continue;
|
|
1320
1529
|
modifiers[name] = {
|
|
1321
1530
|
source: 'meta',
|
|
1322
1531
|
...(attr.default !== undefined ? { default: String(attr.default) } : {}),
|
|
1323
1532
|
};
|
|
1324
1533
|
}
|
|
1534
|
+
// D34 — a root-placed axis's default is the generated config's, since a
|
|
1535
|
+
// template never writes on the root.
|
|
1536
|
+
const d = def.defaults ?? {};
|
|
1325
1537
|
return {
|
|
1538
|
+
composed: true,
|
|
1539
|
+
...(d.width ? { defaultWidth: d.width } : {}),
|
|
1540
|
+
...(d.spacing ? { defaultSpacing: d.spacing } : {}),
|
|
1541
|
+
...(d.inset ? { defaultInset: d.inset } : {}),
|
|
1326
1542
|
...(Object.keys(modifiers).length > 0 ? { modifiers } : {}),
|
|
1327
1543
|
...(def.metaFields ? { metaFields: def.metaFields } : {}),
|
|
1328
1544
|
...(def.blocks ? { blocks: def.blocks } : {}),
|
|
1329
1545
|
...(def.provides ? { provides: def.provides } : {}),
|
|
1330
1546
|
};
|
|
1331
1547
|
}
|
|
1548
|
+
/** The attributes a template passes to a placed rune as an attribute value:
|
|
1549
|
+
* `name=$attrs.x` or `name=$attrs["x"]`. */
|
|
1550
|
+
function forwardedAttributes(template) {
|
|
1551
|
+
const out = new Set();
|
|
1552
|
+
for (const m of template.matchAll(/[\w-]+\s*=\s*\$attrs(?:\.([\w-]+)|\[\s*["']([^"']+)["']\s*\])/g)) {
|
|
1553
|
+
out.add(m[1] ?? m[2]);
|
|
1554
|
+
}
|
|
1555
|
+
return out;
|
|
1556
|
+
}
|
|
1332
1557
|
/** The schema types a rune can emit at its root — its table's, and its `seoType`. */
|
|
1333
1558
|
function emittedTypes(entry, table) {
|
|
1334
1559
|
return [
|