@caelo-cms/shared 0.10.72 → 0.11.1
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/ai-tools.d.ts +103 -142
- package/dist/ai-tools.d.ts.map +1 -1
- package/dist/ai-tools.js +79 -167
- package/dist/ai-tools.js.map +1 -1
- package/dist/build-page.d.ts +115 -35
- package/dist/build-page.d.ts.map +1 -1
- package/dist/build-page.js +31 -12
- package/dist/build-page.js.map +1 -1
- package/dist/content.d.ts +214 -102
- package/dist/content.d.ts.map +1 -1
- package/dist/content.js +219 -84
- package/dist/content.js.map +1 -1
- package/dist/css-scope-scan.js +1 -1
- package/dist/css-var-aliases.d.ts +20 -0
- package/dist/css-var-aliases.d.ts.map +1 -0
- package/dist/css-var-aliases.js +95 -0
- package/dist/css-var-aliases.js.map +1 -0
- package/dist/css-var-scan.d.ts +0 -16
- package/dist/css-var-scan.d.ts.map +1 -1
- package/dist/css-var-scan.js +24 -0
- package/dist/css-var-scan.js.map +1 -1
- package/dist/edit-markers.d.ts +41 -5
- package/dist/edit-markers.d.ts.map +1 -1
- package/dist/edit-markers.js +47 -50
- package/dist/edit-markers.js.map +1 -1
- package/dist/escaped-line-breaks.d.ts +44 -0
- package/dist/escaped-line-breaks.d.ts.map +1 -0
- package/dist/escaped-line-breaks.js +203 -0
- package/dist/escaped-line-breaks.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/interactions.js +6 -6
- package/dist/interactions.js.map +1 -1
- package/dist/link-canonicalise.d.ts.map +1 -1
- package/dist/link-canonicalise.js +7 -1
- package/dist/link-canonicalise.js.map +1 -1
- package/dist/link-refs.d.ts +1 -1
- package/dist/link-refs.js +3 -3
- package/dist/lists/address.d.ts +34 -0
- package/dist/lists/address.d.ts.map +1 -0
- package/dist/lists/address.js +43 -0
- package/dist/lists/address.js.map +1 -0
- package/dist/lists/default-types.d.ts +22 -0
- package/dist/lists/default-types.d.ts.map +1 -0
- package/dist/lists/default-types.js +127 -0
- package/dist/lists/default-types.js.map +1 -0
- package/dist/lists/fields.d.ts +152 -0
- package/dist/lists/fields.d.ts.map +1 -0
- package/dist/lists/fields.js +259 -0
- package/dist/lists/fields.js.map +1 -0
- package/dist/lists/index.d.ts +14 -0
- package/dist/lists/index.d.ts.map +1 -0
- package/dist/lists/index.js +15 -0
- package/dist/lists/index.js.map +1 -0
- package/dist/lists/item-attr.d.ts +26 -0
- package/dist/lists/item-attr.d.ts.map +1 -0
- package/dist/lists/item-attr.js +31 -0
- package/dist/lists/item-attr.js.map +1 -0
- package/dist/lists/items.d.ts +105 -0
- package/dist/lists/items.d.ts.map +1 -0
- package/dist/lists/items.js +304 -0
- package/dist/lists/items.js.map +1 -0
- package/dist/lists/list-type.d.ts +401 -0
- package/dist/lists/list-type.d.ts.map +1 -0
- package/dist/lists/list-type.js +275 -0
- package/dist/lists/list-type.js.map +1 -0
- package/dist/lists/model.d.ts +14 -0
- package/dist/lists/model.d.ts.map +1 -0
- package/dist/lists/model.js +15 -0
- package/dist/lists/model.js.map +1 -0
- package/dist/lists/page-type.d.ts +115 -0
- package/dist/lists/page-type.d.ts.map +1 -0
- package/dist/lists/page-type.js +66 -0
- package/dist/lists/page-type.js.map +1 -0
- package/dist/lists/refs.d.ts +13 -0
- package/dist/lists/refs.d.ts.map +1 -0
- package/dist/lists/refs.js +18 -0
- package/dist/lists/refs.js.map +1 -0
- package/dist/lists/render-data.d.ts +123 -0
- package/dist/lists/render-data.d.ts.map +1 -0
- package/dist/lists/render-data.js +3 -0
- package/dist/lists/render-data.js.map +1 -0
- package/dist/lists/render-deps.d.ts +69 -0
- package/dist/lists/render-deps.d.ts.map +1 -0
- package/dist/lists/render-deps.js +93 -0
- package/dist/lists/render-deps.js.map +1 -0
- package/dist/lists/resolve-values.d.ts +63 -0
- package/dist/lists/resolve-values.d.ts.map +1 -0
- package/dist/lists/resolve-values.js +181 -0
- package/dist/lists/resolve-values.js.map +1 -0
- package/dist/lists/resolve.d.ts +119 -0
- package/dist/lists/resolve.d.ts.map +1 -0
- package/dist/lists/resolve.js +970 -0
- package/dist/lists/resolve.js.map +1 -0
- package/dist/lists/scope.d.ts +81 -0
- package/dist/lists/scope.d.ts.map +1 -0
- package/dist/lists/scope.js +3 -0
- package/dist/lists/scope.js.map +1 -0
- package/dist/markup-structure.d.ts +8 -0
- package/dist/markup-structure.d.ts.map +1 -1
- package/dist/markup-structure.js +2 -0
- package/dist/markup-structure.js.map +1 -1
- package/dist/media.d.ts +1 -1
- package/dist/nested-module-render.d.ts +38 -20
- package/dist/nested-module-render.d.ts.map +1 -1
- package/dist/nested-module-render.js +156 -31
- package/dist/nested-module-render.js.map +1 -1
- package/dist/page-log.d.ts +1 -1
- package/dist/preview-compose.d.ts +24 -21
- package/dist/preview-compose.d.ts.map +1 -1
- package/dist/preview-compose.js +34 -118
- package/dist/preview-compose.js.map +1 -1
- package/dist/redirect-plan.d.ts +13 -0
- package/dist/redirect-plan.d.ts.map +1 -1
- package/dist/redirect-plan.js +22 -0
- package/dist/redirect-plan.js.map +1 -1
- package/dist/seo.d.ts +21 -0
- package/dist/seo.d.ts.map +1 -1
- package/dist/seo.js +32 -0
- package/dist/seo.js.map +1 -1
- package/dist/source-text-transport.d.ts +43 -0
- package/dist/source-text-transport.d.ts.map +1 -0
- package/dist/source-text-transport.js +79 -0
- package/dist/source-text-transport.js.map +1 -0
- package/dist/template-engine.d.ts +44 -31
- package/dist/template-engine.d.ts.map +1 -1
- package/dist/template-engine.js +75 -174
- package/dist/template-engine.js.map +1 -1
- package/dist/template-sections.d.ts +69 -0
- package/dist/template-sections.d.ts.map +1 -0
- package/dist/template-sections.js +436 -0
- package/dist/template-sections.js.map +1 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +8 -1
- package/src/__tests__/redos-hardening.test.ts +51 -16
- package/src/ai-tools-add-module-modes.test.ts +51 -1
- package/src/ai-tools.test.ts +7 -10
- package/src/ai-tools.ts +90 -180
- package/src/bug-intake.test.ts +1 -1
- package/src/build-page.test.ts +34 -3
- package/src/build-page.ts +34 -11
- package/src/content.test.ts +137 -4
- package/src/content.ts +230 -85
- package/src/css-scope-scan.ts +1 -1
- package/src/css-var-aliases.test.ts +61 -0
- package/src/css-var-aliases.ts +109 -0
- package/src/css-var-scan.ts +9 -0
- package/src/edit-markers.test.ts +37 -21
- package/src/edit-markers.ts +52 -20
- package/src/escaped-line-breaks.ts +221 -0
- package/src/index.ts +4 -1
- package/src/interactions.ts +6 -6
- package/src/link-canonicalise.test.ts +36 -6
- package/src/link-canonicalise.ts +6 -1
- package/src/link-refs.test.ts +67 -42
- package/src/link-refs.ts +3 -3
- package/src/lists/address.ts +75 -0
- package/src/lists/default-types.ts +141 -0
- package/src/lists/fields.ts +286 -0
- package/src/lists/index.ts +16 -0
- package/src/lists/item-attr.test.ts +36 -0
- package/src/lists/item-attr.ts +49 -0
- package/src/lists/items.ts +402 -0
- package/src/lists/list-fixtures.test.ts +308 -0
- package/src/lists/list-type.ts +306 -0
- package/src/lists/lists.test.ts +328 -0
- package/src/lists/model.ts +16 -0
- package/src/lists/page-type.ts +71 -0
- package/src/lists/refs.ts +21 -0
- package/src/lists/render-data.ts +134 -0
- package/src/lists/render-deps.test.ts +132 -0
- package/src/lists/render-deps.ts +147 -0
- package/src/lists/resolve-values.ts +224 -0
- package/src/lists/resolve.test.ts +472 -0
- package/src/lists/resolve.ts +1246 -0
- package/src/lists/scope.ts +69 -0
- package/src/lists-compose-parity.test.ts +262 -0
- package/src/markup-structure.ts +10 -0
- package/src/nested-module-render.test.ts +147 -74
- package/src/nested-module-render.ts +120 -48
- package/src/preview-compose.test.ts +55 -19
- package/src/preview-compose.ts +59 -154
- package/src/redirect-plan.test.ts +25 -0
- package/src/redirect-plan.ts +24 -0
- package/src/seo.test.ts +58 -0
- package/src/seo.ts +49 -0
- package/src/source-text-transport.test.ts +158 -0
- package/src/source-text-transport.ts +81 -0
- package/src/template-engine-lists.test.ts +608 -0
- package/src/template-engine.test.ts +62 -80
- package/src/template-engine.ts +102 -245
- package/src/template-sections.ts +514 -0
- package/src/version.ts +1 -1
- package/dist/structured-sets.d.ts +0 -70
- package/dist/structured-sets.d.ts.map +0 -1
- package/dist/structured-sets.js +0 -92
- package/dist/structured-sets.js.map +0 -1
- package/src/structured-sets.ts +0 -114
- package/src/template-engine-data-lists.test.ts +0 -90
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The values a template sees inside a list section (epic #1107,
|
|
5
|
+
* `docs/lists.md` §8) — the seam between the resolver (`resolve.ts`),
|
|
6
|
+
* which turns stored entries, page queries, system and plugin sources
|
|
7
|
+
* into these scopes, and the template engine, which only looks keys up.
|
|
8
|
+
* The engine knows nothing about list types, links or pages; the
|
|
9
|
+
* resolver knows nothing about template syntax.
|
|
10
|
+
*
|
|
11
|
+
* Every lookup is lazy: a children list, a query or an embedded module
|
|
12
|
+
* is only resolved when the template actually reaches it, so render
|
|
13
|
+
* dependencies (`ListRenderDeps`) record what a page really rendered,
|
|
14
|
+
* not what it could have.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** One value a template key resolves to. */
|
|
18
|
+
export type ScopeValue =
|
|
19
|
+
/** Plain text (`{{f}}`); a section over it renders when it is non-empty. */
|
|
20
|
+
| { readonly kind: "text"; readonly text: string }
|
|
21
|
+
/** A flag (`{{#is_current}}`); `{{f}}` renders `true` / `false`. */
|
|
22
|
+
| { readonly kind: "bool"; readonly value: boolean }
|
|
23
|
+
/**
|
|
24
|
+
* A compound value with dot access (`link`, `media`, `date`, `props`,
|
|
25
|
+
* `page`). `text` is what the bare key renders (`{{link}}` = href);
|
|
26
|
+
* null when the bare key has no meaning (`{{props}}`).
|
|
27
|
+
*/
|
|
28
|
+
| {
|
|
29
|
+
readonly kind: "record";
|
|
30
|
+
readonly text: string | null;
|
|
31
|
+
/** Section truthiness of the bare key (`{{#link}}` — set or not). */
|
|
32
|
+
readonly present: boolean;
|
|
33
|
+
get(key: string): ScopeValue | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Iterable entries (`children`, `items`, a term field, `page.terms.tags`). */
|
|
36
|
+
| { readonly kind: "list"; readonly entries: readonly ScopeEntry[] }
|
|
37
|
+
/** An embedded module (`{{>f}}`). */
|
|
38
|
+
| {
|
|
39
|
+
readonly kind: "module";
|
|
40
|
+
readonly ref: { readonly moduleId: string; readonly contentInstanceId: string };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A value that could not resolve (a list source of an inactive plugin,
|
|
44
|
+
* a malformed query). The engine renders the tag raw and reports
|
|
45
|
+
* `marker` — loud, never silently empty (CLAUDE.md §2).
|
|
46
|
+
*/
|
|
47
|
+
| { readonly kind: "failed"; readonly marker: string };
|
|
48
|
+
|
|
49
|
+
/** One entry a list section iterates. */
|
|
50
|
+
export interface ScopeEntry {
|
|
51
|
+
/** The entry id (stored uuid, page id for page-shaped entries, plugin id). */
|
|
52
|
+
readonly id: string;
|
|
53
|
+
/** Look up a key of this entry: fields, system keys, children, `items`. */
|
|
54
|
+
get(key: string): ScopeValue | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Value of the `data-caelo-list-item` attribute stamped on the entry's
|
|
57
|
+
* root element (`docs/lists.md` §13), or null for generated entries that
|
|
58
|
+
* are no stored list entry (query results, plugin entries).
|
|
59
|
+
*/
|
|
60
|
+
readonly itemAttr: string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Loop name of this entry's stored children (`children.name` of its
|
|
63
|
+
* node), which `{{>recurse}}` renders with the enclosing block; null
|
|
64
|
+
* when the node takes no children.
|
|
65
|
+
*/
|
|
66
|
+
readonly childrenKey: string | null;
|
|
67
|
+
/** 1-based depth in its tree. */
|
|
68
|
+
readonly depth: number;
|
|
69
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Preview / generator parity for lists (epic #1107, #1110 acceptance):
|
|
5
|
+
* a mega menu (entry → column / news feed / promo → sub link, with an
|
|
6
|
+
* embedded teaser module) rendered as a LAYOUT chrome placement and
|
|
7
|
+
* inside a NESTED module, composed the way the editor preview does it
|
|
8
|
+
* (page modules pre-rendered with `renderModuleWithContent`, chrome
|
|
9
|
+
* through the composer) and the way the static generator does it
|
|
10
|
+
* (everything through the composer with a `nestedModules` resolver).
|
|
11
|
+
* Same input, same HTML.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, expect, it } from "bun:test";
|
|
15
|
+
import {
|
|
16
|
+
fixtureData,
|
|
17
|
+
fixtureLinkIndex,
|
|
18
|
+
INSTANCE,
|
|
19
|
+
ITEM,
|
|
20
|
+
LIST,
|
|
21
|
+
MODULE,
|
|
22
|
+
PAGE,
|
|
23
|
+
} from "./lists/list-fixtures.test.js";
|
|
24
|
+
import { createListRenderContext } from "./lists/resolve.js";
|
|
25
|
+
import {
|
|
26
|
+
type NestedContentInstanceResource,
|
|
27
|
+
type NestedModuleResource,
|
|
28
|
+
type NestedRenderResolver,
|
|
29
|
+
renderModuleWithContent,
|
|
30
|
+
} from "./nested-module-render.js";
|
|
31
|
+
import { type ComposeModule, composePageWithLayout } from "./preview-compose.js";
|
|
32
|
+
import type { TemplateField } from "./template-engine.js";
|
|
33
|
+
|
|
34
|
+
const HEADER_MOD = "00000000-0000-4000-8000-000000000311";
|
|
35
|
+
const WRAPPER_MOD = "00000000-0000-4000-8000-000000000312";
|
|
36
|
+
const HEADER_CI = "00000000-0000-4000-8000-000000000411";
|
|
37
|
+
const WRAPPER_CI = "00000000-0000-4000-8000-000000000412";
|
|
38
|
+
|
|
39
|
+
const MEGA_HTML =
|
|
40
|
+
'<nav class="mega"><ul>{{#mega}}<li class="{{#is_current}}current{{/is_current}}{{#is_ancestor}}open{{/is_ancestor}}">' +
|
|
41
|
+
'<a href="{{link}}">{{label}}</a>' +
|
|
42
|
+
'{{#has_columns}}<div class="panel">{{#columns}}' +
|
|
43
|
+
'{{#is_column}}<div class="col"><h4>{{heading}}</h4><ul>{{#links}}<li><a href="{{link}}">{{label}}</a>{{#badge_hot}}!{{/badge_hot}}</li>{{/links}}</ul></div>{{/is_column}}' +
|
|
44
|
+
'{{#is_news_feed}}<aside><h4>{{heading}}</h4>{{#items}}<a href="{{link}}">{{title}} · {{published_at.formatted}}</a>{{/items}}{{^items}}No news{{/items}}</aside>{{/is_news_feed}}' +
|
|
45
|
+
'{{#is_promo}}<div class="promo">{{>teaser}}</div>{{/is_promo}}' +
|
|
46
|
+
"{{/columns}}</div>{{/has_columns}}</li>{{/mega}}</ul><p>{{page.title}}</p></nav>";
|
|
47
|
+
|
|
48
|
+
const HEADER_FIELDS: TemplateField[] = [
|
|
49
|
+
{
|
|
50
|
+
name: "mega",
|
|
51
|
+
kind: "list",
|
|
52
|
+
listType: "mega-menu",
|
|
53
|
+
bind: "shared",
|
|
54
|
+
list: `caelo:list/${LIST.mega}`,
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const modules: NestedModuleResource[] = [
|
|
59
|
+
{
|
|
60
|
+
moduleId: HEADER_MOD,
|
|
61
|
+
slug: "mega-header",
|
|
62
|
+
html: MEGA_HTML,
|
|
63
|
+
css: ".mega{}",
|
|
64
|
+
js: "",
|
|
65
|
+
fields: HEADER_FIELDS,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
moduleId: MODULE.teaser,
|
|
69
|
+
slug: "teaser",
|
|
70
|
+
html: '<article class="teaser"><h5>{{title}}</h5></article>',
|
|
71
|
+
css: ".teaser{}",
|
|
72
|
+
js: "",
|
|
73
|
+
fields: [{ name: "title", kind: "text" }],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
moduleId: WRAPPER_MOD,
|
|
77
|
+
slug: "wrapper",
|
|
78
|
+
html: '<section class="wrap">{{>inner}}</section>',
|
|
79
|
+
css: ".wrap{}",
|
|
80
|
+
js: "",
|
|
81
|
+
fields: [{ name: "inner", kind: "module" }],
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
const instances: NestedContentInstanceResource[] = [
|
|
85
|
+
{
|
|
86
|
+
id: INSTANCE.teaser,
|
|
87
|
+
moduleId: MODULE.teaser,
|
|
88
|
+
values: { title: "Try it free" },
|
|
89
|
+
deletedAt: null,
|
|
90
|
+
},
|
|
91
|
+
{ id: HEADER_CI, moduleId: HEADER_MOD, values: {}, deletedAt: null },
|
|
92
|
+
{
|
|
93
|
+
id: WRAPPER_CI,
|
|
94
|
+
moduleId: WRAPPER_MOD,
|
|
95
|
+
values: { inner: { moduleId: HEADER_MOD, contentInstanceId: HEADER_CI } },
|
|
96
|
+
deletedAt: null,
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
const resolver: NestedRenderResolver = {
|
|
100
|
+
getModule: (id) => modules.find((m) => m.moduleId === id) ?? null,
|
|
101
|
+
getContentInstance: (id) => instances.find((c) => c.id === id) ?? null,
|
|
102
|
+
};
|
|
103
|
+
const mod = (id: string): NestedModuleResource =>
|
|
104
|
+
modules.find((m) => m.moduleId === id) as NestedModuleResource;
|
|
105
|
+
|
|
106
|
+
const LAYOUT =
|
|
107
|
+
'<html><head></head><body><caelo-slot name="header"></caelo-slot><caelo-slot name="content"></caelo-slot></body></html>';
|
|
108
|
+
const TEMPLATE = '<body><caelo-slot name="content"></caelo-slot></body>';
|
|
109
|
+
|
|
110
|
+
/** The chrome placement: renders from field defaults, no content instance. */
|
|
111
|
+
function chrome(): ComposeModule {
|
|
112
|
+
const m = mod(HEADER_MOD);
|
|
113
|
+
return {
|
|
114
|
+
moduleId: m.moduleId,
|
|
115
|
+
slug: m.slug,
|
|
116
|
+
displayName: "Header",
|
|
117
|
+
html: m.html,
|
|
118
|
+
css: m.css,
|
|
119
|
+
js: m.js,
|
|
120
|
+
fields: m.fields,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function contexts(pageId: string) {
|
|
125
|
+
const linkResolver = fixtureLinkIndex().forPage(pageId);
|
|
126
|
+
const lists = createListRenderContext(fixtureData()).forPage(pageId, { linkResolver });
|
|
127
|
+
return { linkResolver, lists };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** The editor preview's path (`pages.render_preview`). */
|
|
131
|
+
function previewCompose(pageId: string) {
|
|
132
|
+
const { linkResolver, lists } = contexts(pageId);
|
|
133
|
+
const rendered = renderModuleWithContent(
|
|
134
|
+
WRAPPER_MOD,
|
|
135
|
+
WRAPPER_CI,
|
|
136
|
+
resolver,
|
|
137
|
+
undefined,
|
|
138
|
+
lists,
|
|
139
|
+
linkResolver,
|
|
140
|
+
);
|
|
141
|
+
const w = mod(WRAPPER_MOD);
|
|
142
|
+
const nested = [...rendered.touchedModuleIds].filter((id) => id !== WRAPPER_MOD).map(mod);
|
|
143
|
+
return composePageWithLayout({
|
|
144
|
+
templateHtml: TEMPLATE,
|
|
145
|
+
templateCss: "",
|
|
146
|
+
blocks: [
|
|
147
|
+
{
|
|
148
|
+
blockName: "content",
|
|
149
|
+
modules: [
|
|
150
|
+
{
|
|
151
|
+
moduleId: w.moduleId,
|
|
152
|
+
slug: w.slug,
|
|
153
|
+
displayName: "Wrapper",
|
|
154
|
+
html: rendered.html,
|
|
155
|
+
css: w.css,
|
|
156
|
+
js: w.js,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
blockName: "__nested__",
|
|
162
|
+
modules: nested.map((n) => ({
|
|
163
|
+
moduleId: n.moduleId,
|
|
164
|
+
slug: n.slug,
|
|
165
|
+
displayName: n.slug,
|
|
166
|
+
html: "",
|
|
167
|
+
css: n.css,
|
|
168
|
+
js: n.js,
|
|
169
|
+
})),
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
layoutHtml: LAYOUT,
|
|
173
|
+
layoutCss: "",
|
|
174
|
+
layoutBlocks: [{ blockName: "header", modules: [chrome()] }],
|
|
175
|
+
nestedModules: resolver,
|
|
176
|
+
lists,
|
|
177
|
+
linkResolver,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The static generator's path (`generate.ts`). */
|
|
182
|
+
function generatorCompose(pageId: string) {
|
|
183
|
+
const { linkResolver, lists } = contexts(pageId);
|
|
184
|
+
const w = mod(WRAPPER_MOD);
|
|
185
|
+
return composePageWithLayout({
|
|
186
|
+
templateHtml: TEMPLATE,
|
|
187
|
+
templateCss: "",
|
|
188
|
+
blocks: [
|
|
189
|
+
{
|
|
190
|
+
blockName: "content",
|
|
191
|
+
modules: [
|
|
192
|
+
{
|
|
193
|
+
moduleId: w.moduleId,
|
|
194
|
+
slug: w.slug,
|
|
195
|
+
displayName: "Wrapper",
|
|
196
|
+
html: w.html,
|
|
197
|
+
css: w.css,
|
|
198
|
+
js: w.js,
|
|
199
|
+
fields: w.fields,
|
|
200
|
+
contentValues: (
|
|
201
|
+
instances.find((c) => c.id === WRAPPER_CI) as NestedContentInstanceResource
|
|
202
|
+
).values,
|
|
203
|
+
contentInstanceId: WRAPPER_CI,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
layoutHtml: LAYOUT,
|
|
209
|
+
layoutCss: "",
|
|
210
|
+
layoutBlocks: [{ blockName: "header", modules: [chrome()] }],
|
|
211
|
+
nestedModules: resolver,
|
|
212
|
+
lists,
|
|
213
|
+
linkResolver,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const bodyOf = (html: string) => html.slice(html.indexOf("<body>"), html.indexOf("</body>"));
|
|
218
|
+
|
|
219
|
+
describe("mega menu — preview / generator parity", () => {
|
|
220
|
+
it("renders the same HTML on both paths, in chrome and inside a nested module", () => {
|
|
221
|
+
const preview = previewCompose(PAGE.team);
|
|
222
|
+
const build = generatorCompose(PAGE.team);
|
|
223
|
+
expect(preview.moduleFailures).toEqual([]);
|
|
224
|
+
expect(build.moduleFailures).toEqual([]);
|
|
225
|
+
expect(bodyOf(preview.html)).toBe(bodyOf(build.html));
|
|
226
|
+
// CSS of every module the lists reached joins the page once on both paths.
|
|
227
|
+
for (const out of [preview, build]) {
|
|
228
|
+
for (const css of [".mega{}", ".teaser{}", ".wrap{}"]) {
|
|
229
|
+
expect(out.html.split(css).length - 1).toBe(1);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("renders every node kind with page-aware state", () => {
|
|
235
|
+
const body = bodyOf(generatorCompose(PAGE.team).html);
|
|
236
|
+
// Twice: once in the chrome, once inside the wrapper module.
|
|
237
|
+
expect(body.split('<nav class="mega"').length - 1).toBe(2);
|
|
238
|
+
expect(body).toContain(
|
|
239
|
+
`<li class="open" data-caelo-list-item="${LIST.mega}/${ITEM.products}"><a href="/about/">Products</a>`,
|
|
240
|
+
);
|
|
241
|
+
expect(body).toContain(
|
|
242
|
+
`<div class="col" data-caelo-list-item="${LIST.mega}/${ITEM.column}"><h4>Explore</h4>`,
|
|
243
|
+
);
|
|
244
|
+
expect(body).toContain(
|
|
245
|
+
`<li data-caelo-list-item="${LIST.mega}/${ITEM.subA}"><a href="/about/team/">Team</a></li>`,
|
|
246
|
+
);
|
|
247
|
+
expect(body).toContain(
|
|
248
|
+
'<a href="/news/meetup/">Meetup · October 5, 2026</a><a href="/news/launch/">Launch day · October 1, 2026</a>',
|
|
249
|
+
);
|
|
250
|
+
expect(body).toContain(
|
|
251
|
+
`<div class="promo" data-caelo-list-item="${LIST.mega}/${ITEM.promo}"><article class="teaser" data-caelo-module-id="${MODULE.teaser}"><h5>Try it free</h5></article></div>`,
|
|
252
|
+
);
|
|
253
|
+
expect(body).toContain("<p>Team</p>");
|
|
254
|
+
expect(body).not.toContain("{{");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("re-renders per page: the page title and current state follow the rendering page", () => {
|
|
258
|
+
const body = bodyOf(generatorCompose(PAGE.home).html);
|
|
259
|
+
expect(body).toContain("<p>Home</p>");
|
|
260
|
+
expect(body).toContain('<li class=""');
|
|
261
|
+
});
|
|
262
|
+
});
|
package/src/markup-structure.ts
CHANGED
|
@@ -40,6 +40,14 @@ export interface MarkupNode {
|
|
|
40
40
|
readonly classes: readonly string[];
|
|
41
41
|
/** The `data-caelo-module-id` the composer stamps on every module root. */
|
|
42
42
|
readonly moduleId: string | null;
|
|
43
|
+
/** The `href` attribute (links), as written. */
|
|
44
|
+
readonly href: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* The `data-caelo-list-item` the template engine stamps on a rendered
|
|
47
|
+
* list entry (docs/lists.md §13): `<listId>/<itemId>` for a shared
|
|
48
|
+
* list, `instance/…` / `local/…` for a module's local list.
|
|
49
|
+
*/
|
|
50
|
+
readonly listItem: string | null;
|
|
43
51
|
readonly children: MarkupNode[];
|
|
44
52
|
readonly parent: MarkupNode | null;
|
|
45
53
|
}
|
|
@@ -76,6 +84,8 @@ export function parseMarkup(html: string): MarkupNode[] {
|
|
|
76
84
|
tag,
|
|
77
85
|
classes: splitClasses(attribs.class),
|
|
78
86
|
moduleId: attribs["data-caelo-module-id"] ?? null,
|
|
87
|
+
href: attribs.href ?? null,
|
|
88
|
+
listItem: attribs["data-caelo-list-item"] ?? null,
|
|
79
89
|
children: [],
|
|
80
90
|
parent,
|
|
81
91
|
};
|