@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,61 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "bun:test";
|
|
4
|
+
import {
|
|
5
|
+
formatCssVarRenameReport,
|
|
6
|
+
renameCssVarAliases,
|
|
7
|
+
themeVarForAlias,
|
|
8
|
+
} from "./css-var-aliases.js";
|
|
9
|
+
import { scanCssVars } from "./css-var-scan.js";
|
|
10
|
+
|
|
11
|
+
const THEME = [
|
|
12
|
+
"--color-background",
|
|
13
|
+
"--color-foreground",
|
|
14
|
+
"--color-muted",
|
|
15
|
+
"--color-muted-foreground",
|
|
16
|
+
"--color-border",
|
|
17
|
+
"--color-accent",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
describe("renameCssVarAliases", () => {
|
|
21
|
+
// The header CSS of e2e-livedit run 40351690739.
|
|
22
|
+
const css =
|
|
23
|
+
".site-header{border-bottom:1px solid var(--color-border, #e5e5e5);background:var(--color-surface, #fff);}\n" +
|
|
24
|
+
".site-header__brand{color:var(--color-text, #111);}\n" +
|
|
25
|
+
".site-header__panel{background:var(--color-surface, #fff);}\n" +
|
|
26
|
+
".site-header__col h3{color:var(--color-text-muted,#888);}";
|
|
27
|
+
|
|
28
|
+
it("renames common role names to the theme's names, every occurrence", () => {
|
|
29
|
+
const r = renameCssVarAliases(css, THEME, new Set());
|
|
30
|
+
expect(r.css).not.toContain("--color-surface");
|
|
31
|
+
expect(r.css).not.toContain("var(--color-text,");
|
|
32
|
+
expect(r.css).toContain("background:var(--color-background, #fff)");
|
|
33
|
+
expect(r.css).toContain("color:var(--color-foreground, #111)");
|
|
34
|
+
expect(r.css).toContain("color:var(--color-muted-foreground,#888)");
|
|
35
|
+
expect(r.renames).toEqual([
|
|
36
|
+
{ from: "--color-surface", to: "--color-background", count: 2 },
|
|
37
|
+
{ from: "--color-text", to: "--color-foreground", count: 1 },
|
|
38
|
+
{ from: "--color-text-muted", to: "--color-muted-foreground", count: 1 },
|
|
39
|
+
]);
|
|
40
|
+
expect(formatCssVarRenameReport(r.renames)).toContain("--color-surface→--color-background×2");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("leaves known names, the author's own properties and unmapped names alone", () => {
|
|
44
|
+
const own = ":root{--surface:#fafafa}.a{background:var(--surface);color:var(--brand-ink)}";
|
|
45
|
+
const r = renameCssVarAliases(own, THEME, new Set(["--surface"]));
|
|
46
|
+
expect(r.css).toBe(own);
|
|
47
|
+
expect(r.renames).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("renames only when the theme emits the target", () => {
|
|
51
|
+
expect(themeVarForAlias("--color-surface-raised", new Set(THEME))).toBeNull();
|
|
52
|
+
expect(themeVarForAlias("--text", new Set(THEME))).toBe("--color-foreground");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("scanCssVars suggestions", () => {
|
|
57
|
+
it("suggests the theme's role name, not the nearest spelling", () => {
|
|
58
|
+
const [u] = scanCssVars({ css: ".a{color:var(--color-text, #111)}", knownVars: THEME });
|
|
59
|
+
expect(u?.suggestion).toBe("--color-foreground");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Common names for theme roles, mapped to the names Caelo's theme emits.
|
|
5
|
+
*
|
|
6
|
+
* Module CSS is authored against conventions the model knows from other
|
|
7
|
+
* design systems: `--color-text`, `--color-surface`, `--color-text-muted`.
|
|
8
|
+
* Caelo's theme calls those roles `--color-foreground`, `--color-background`
|
|
9
|
+
* and `--color-muted-foreground`. The write-time scan used to answer with an
|
|
10
|
+
* edit-distance guess (`--color-text` → "did you mean `--color-accent`?"),
|
|
11
|
+
* so the AI loaded the theme and renamed every reference with edit_content
|
|
12
|
+
* — two to three extra tool calls per module, and a failed one when the
|
|
13
|
+
* same declaration appeared twice (e2e-livedit run 40351690739). A name
|
|
14
|
+
* that unambiguously means a role the theme has is renamed before the
|
|
15
|
+
* write instead, and the tool result says so, the same way theme-literal
|
|
16
|
+
* binding rewrites a literal that equals a token (issue #164). Only names
|
|
17
|
+
* in this table are touched, and only when the theme emits the target.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Role name (without `--` and an optional `color-` prefix) → theme role. */
|
|
21
|
+
const ROLE_ALIASES: Readonly<Record<string, string>> = {
|
|
22
|
+
text: "foreground",
|
|
23
|
+
"text-primary": "foreground",
|
|
24
|
+
"text-color": "foreground",
|
|
25
|
+
"body-text": "foreground",
|
|
26
|
+
fg: "foreground",
|
|
27
|
+
heading: "foreground",
|
|
28
|
+
"text-muted": "muted-foreground",
|
|
29
|
+
"text-secondary": "muted-foreground",
|
|
30
|
+
"text-subtle": "muted-foreground",
|
|
31
|
+
"muted-text": "muted-foreground",
|
|
32
|
+
"fg-muted": "muted-foreground",
|
|
33
|
+
surface: "background",
|
|
34
|
+
bg: "background",
|
|
35
|
+
"page-bg": "background",
|
|
36
|
+
"body-bg": "background",
|
|
37
|
+
"surface-muted": "muted",
|
|
38
|
+
"surface-alt": "muted",
|
|
39
|
+
"bg-muted": "muted",
|
|
40
|
+
"bg-subtle": "muted",
|
|
41
|
+
"surface-raised": "card",
|
|
42
|
+
"card-bg": "card",
|
|
43
|
+
"border-color": "border",
|
|
44
|
+
divider: "border",
|
|
45
|
+
line: "border",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** The theme var an unknown name stands for, or null. */
|
|
49
|
+
export function themeVarForAlias(name: string, knownVars: ReadonlySet<string>): string | null {
|
|
50
|
+
const bare = name.replace(/^--/, "").replace(/^color-/, "");
|
|
51
|
+
const role = ROLE_ALIASES[bare];
|
|
52
|
+
if (role === undefined) return null;
|
|
53
|
+
const target = `--color-${role}`;
|
|
54
|
+
return knownVars.has(target) ? target : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** `var(` + the referenced name, when the name ends there. */
|
|
58
|
+
const VAR_NAME_RE = /(var\(\s*)(--[a-zA-Z0-9_-]+)(?![a-zA-Z0-9_-])/g;
|
|
59
|
+
|
|
60
|
+
/** One renamed reference. */
|
|
61
|
+
export interface CssVarRename {
|
|
62
|
+
readonly from: string;
|
|
63
|
+
readonly to: string;
|
|
64
|
+
readonly count: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Rename `var(--alias…)` references to the theme's name for that role.
|
|
69
|
+
* A name the CSS itself defines (`--surface: …`) is the author's own
|
|
70
|
+
* property and stays.
|
|
71
|
+
*/
|
|
72
|
+
export function renameCssVarAliases(
|
|
73
|
+
css: string,
|
|
74
|
+
knownVars: readonly string[],
|
|
75
|
+
definedInCss: ReadonlySet<string>,
|
|
76
|
+
): { css: string; renames: CssVarRename[] } {
|
|
77
|
+
const known = new Set(knownVars);
|
|
78
|
+
const targets = new Map<string, string>();
|
|
79
|
+
const counts = new Map<string, number>();
|
|
80
|
+
// One pass over the references, no pattern built from a name.
|
|
81
|
+
const out = css.replace(VAR_NAME_RE, (match, open: string, name: string) => {
|
|
82
|
+
if (known.has(name) || definedInCss.has(name)) return match;
|
|
83
|
+
let to = targets.get(name);
|
|
84
|
+
if (to === undefined) {
|
|
85
|
+
const resolved = themeVarForAlias(name, known);
|
|
86
|
+
if (resolved === null) return match;
|
|
87
|
+
to = resolved;
|
|
88
|
+
targets.set(name, to);
|
|
89
|
+
}
|
|
90
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
91
|
+
return `${open}${to}`;
|
|
92
|
+
});
|
|
93
|
+
const renames: CssVarRename[] = [...targets].map(([from, to]) => ({
|
|
94
|
+
from,
|
|
95
|
+
to,
|
|
96
|
+
count: counts.get(from) ?? 0,
|
|
97
|
+
}));
|
|
98
|
+
return { css: out, renames };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Tool-result sentence for the renames, or null. */
|
|
102
|
+
export function formatCssVarRenameReport(renames: readonly CssVarRename[]): string | null {
|
|
103
|
+
if (renames.length === 0) return null;
|
|
104
|
+
return ` 🔗 renamed to the theme's names: ${renames
|
|
105
|
+
.map((r) => `${r.from}→${r.to}${r.count > 1 ? `×${r.count}` : ""}`)
|
|
106
|
+
.join(
|
|
107
|
+
", ",
|
|
108
|
+
)} — the STORED css uses the theme's names, so anchor later edit_content calls on those.`;
|
|
109
|
+
}
|
package/src/css-var-scan.ts
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* that is legitimate authoring, not drift.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { themeVarForAlias } from "./css-var-aliases.js";
|
|
21
|
+
|
|
20
22
|
/** One `var(--x)` reference found in CSS. */
|
|
21
23
|
export interface CssVarReference {
|
|
22
24
|
readonly name: string;
|
|
@@ -97,6 +99,13 @@ export function scanCssVars(input: CssVarScanInput): UnknownCssVar[] {
|
|
|
97
99
|
for (const ref of extractCssVarReferences(input.css)) {
|
|
98
100
|
if (known.has(ref.name) || reported.has(ref.name)) continue;
|
|
99
101
|
reported.add(ref.name);
|
|
102
|
+
// A common role name (`--color-text`) suggests the theme's name for
|
|
103
|
+
// that role, not the nearest spelling (which was `--color-accent`).
|
|
104
|
+
const alias = themeVarForAlias(ref.name, known);
|
|
105
|
+
if (alias !== null) {
|
|
106
|
+
unknown.push({ name: ref.name, hasFallback: ref.hasFallback, suggestion: alias });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
100
109
|
let best: string | null = null;
|
|
101
110
|
let bestDist = Number.POSITIVE_INFINITY;
|
|
102
111
|
for (const candidate of known) {
|
package/src/edit-markers.test.ts
CHANGED
|
@@ -2,19 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "bun:test";
|
|
4
4
|
import { finalizeEditMarkers, wrapEditableValues } from "./edit-markers.js";
|
|
5
|
-
import {
|
|
5
|
+
import { DEFAULT_LIST_TYPES } from "./lists/default-types.js";
|
|
6
|
+
import { fixtureData } from "./lists/list-fixtures.test.js";
|
|
7
|
+
import { createListRenderContext } from "./lists/resolve.js";
|
|
8
|
+
import { renderTemplate, type TemplateField } from "./template-engine.js";
|
|
6
9
|
|
|
7
|
-
const FIELDS = [
|
|
10
|
+
const FIELDS: TemplateField[] = [
|
|
8
11
|
{ name: "title", kind: "text" },
|
|
9
|
-
{ name: "tags", kind: "
|
|
10
|
-
{ name: "links", kind: "
|
|
12
|
+
{ name: "tags", kind: "list", listType: "texts" },
|
|
13
|
+
{ name: "links", kind: "list", listType: "links" },
|
|
11
14
|
{ name: "photo", kind: "image" },
|
|
12
|
-
]
|
|
15
|
+
];
|
|
16
|
+
const listTypes = (slug: string) => DEFAULT_LIST_TYPES.find((t) => t.slug === slug);
|
|
17
|
+
const lists = createListRenderContext(fixtureData()).forPage(null);
|
|
18
|
+
|
|
19
|
+
const T1 = "00000000-0000-4000-9000-000000000001";
|
|
20
|
+
const T2 = "00000000-0000-4000-9000-000000000002";
|
|
21
|
+
const L1 = "00000000-0000-4000-9000-000000000003";
|
|
22
|
+
const tags = (...t: string[]) =>
|
|
23
|
+
t.map((text, i) => ({ id: [T1, T2][i], node: "text", values: { text } }));
|
|
24
|
+
const links = (label: string, href: string) => [
|
|
25
|
+
{ id: L1, node: "link", values: { label, link: { href } } },
|
|
26
|
+
];
|
|
27
|
+
/** A marker path as the comment encodes it (`-` → %2D). */
|
|
28
|
+
const enc = (p: string) => encodeURIComponent(p).replace(/-/g, "%2D");
|
|
13
29
|
|
|
14
30
|
function render(html: string, values: Record<string, unknown>, sharedPages = 0): string {
|
|
15
|
-
const wrapped = wrapEditableValues(values, FIELDS);
|
|
16
|
-
const rendered = renderTemplate({ html, fields:
|
|
17
|
-
return finalizeEditMarkers(rendered, "content#2", sharedPages)
|
|
31
|
+
const wrapped = wrapEditableValues(values, FIELDS, listTypes);
|
|
32
|
+
const rendered = renderTemplate({ html, fields: FIELDS, contentValues: wrapped, lists }).html;
|
|
33
|
+
return finalizeEditMarkers(rendered, "content#2", sharedPages).replace(
|
|
34
|
+
/ data-caelo-list-item="[^"]*"/g,
|
|
35
|
+
"",
|
|
36
|
+
);
|
|
18
37
|
}
|
|
19
38
|
|
|
20
39
|
const marked = (fields: string, shared = 0) =>
|
|
@@ -34,17 +53,14 @@ describe("edit markers (#917)", () => {
|
|
|
34
53
|
expect(render("<h1>{{title}}</h1>", { title: "Hi" }, 4)).toContain(open("title", 4));
|
|
35
54
|
});
|
|
36
55
|
|
|
37
|
-
it("marks
|
|
56
|
+
it("marks local list entries' text and link targets by entry id", () => {
|
|
38
57
|
const html = render(
|
|
39
|
-
'<ul>{{#tags}}<li>{{
|
|
40
|
-
{
|
|
41
|
-
tags: ["a", "b"],
|
|
42
|
-
links: [{ label: "Docs", href: "/docs" }],
|
|
43
|
-
},
|
|
58
|
+
'<ul>{{#tags}}<li>{{text}}</li>{{/tags}}</ul><nav>{{#links}}<a href="{{link}}">{{label}}</a>{{/links}}</nav>',
|
|
59
|
+
{ tags: tags("a", "b"), links: links("Docs", "/docs") },
|
|
44
60
|
);
|
|
45
|
-
expect(html).toContain(`<li>${open(
|
|
61
|
+
expect(html).toContain(`<li>${open(enc(`tags.${T2}.text`))}b${close}</li>`);
|
|
46
62
|
expect(html).toContain(
|
|
47
|
-
`<a href="/docs"${marked(
|
|
63
|
+
`<a href="/docs"${marked(`href=${enc(`links.${L1}.link.href`)}`)}>${open(enc(`links.${L1}.label`))}Docs${close}</a>`,
|
|
48
64
|
);
|
|
49
65
|
});
|
|
50
66
|
|
|
@@ -61,12 +77,12 @@ describe("edit markers (#917)", () => {
|
|
|
61
77
|
|
|
62
78
|
it("marks link targets, images and alt text on their tag (#918)", () => {
|
|
63
79
|
const html = render(
|
|
64
|
-
'{{#links}}<a href="{{
|
|
65
|
-
{ links:
|
|
80
|
+
'{{#links}}<a href="{{link}}">{{label}}</a>{{/links}}<img src="{{photo}}" alt="{{title}}"/>',
|
|
81
|
+
{ links: links("Docs", "/docs"), photo: "/m/cup.webp", title: "A cup" },
|
|
66
82
|
2,
|
|
67
83
|
);
|
|
68
84
|
expect(html).toContain(
|
|
69
|
-
`<a href="/docs"${marked(
|
|
85
|
+
`<a href="/docs"${marked(`href=${enc(`links.${L1}.link.href`)}`, 2)}>${open(enc(`links.${L1}.label`), 2)}Docs${close}</a>`,
|
|
70
86
|
);
|
|
71
87
|
expect(html).toContain(
|
|
72
88
|
`<img src="/m/cup.webp" alt="A cup"${marked("src=photo alt=title", 2)}/>`,
|
|
@@ -80,9 +96,9 @@ describe("edit markers (#917)", () => {
|
|
|
80
96
|
});
|
|
81
97
|
|
|
82
98
|
it("does not mark values that carry markup, empty values, or non-text kinds", () => {
|
|
83
|
-
const html = render("<h1>{{title}}</h1><p>{{#tags}}{{
|
|
99
|
+
const html = render("<h1>{{title}}</h1><p>{{#tags}}{{text}}{{/tags}}</p>", {
|
|
84
100
|
title: "<b>Bold</b>",
|
|
85
|
-
tags:
|
|
101
|
+
tags: tags(" "),
|
|
86
102
|
});
|
|
87
103
|
expect(html).toBe("<h1><b>Bold</b></h1><p> </p>");
|
|
88
104
|
expect(wrapEditableValues({ n: 3 }, [{ name: "n", kind: "number" }])).toEqual({ n: 3 });
|
package/src/edit-markers.ts
CHANGED
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
* be edited as plain text without destroying that markup.
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
+
import type { ListTypeDef } from "./lists/list-type.js";
|
|
33
|
+
|
|
32
34
|
const START = "\uE000";
|
|
33
35
|
const SEP = "\uE001";
|
|
34
36
|
const END = "\uE002";
|
|
@@ -38,21 +40,29 @@ const SENTINELS = /[\uE000-\uE002]/g;
|
|
|
38
40
|
export const EDIT_MARKER_PREFIX = "caelo:f";
|
|
39
41
|
|
|
40
42
|
/**
|
|
41
|
-
* Field kinds the preview marks: text (`text`, `text
|
|
42
|
-
* `
|
|
43
|
-
* `
|
|
43
|
+
* Field kinds the preview marks: text (`text`, the `text` fields of a
|
|
44
|
+
* local `list`'s entries) and link / image targets (`url`, `image`, the
|
|
45
|
+
* `href` of a local list entry's `link` fields, #918) — the latter
|
|
46
|
+
* usually land in `href` / `src`.
|
|
44
47
|
*/
|
|
45
|
-
const EDITABLE_KINDS = new Set(["text", "
|
|
48
|
+
const EDITABLE_KINDS = new Set(["text", "url", "image", "list"]);
|
|
46
49
|
|
|
47
50
|
/** Attributes whose marked value the editor can change (link target, image, alt text). */
|
|
48
51
|
const MARKABLE_ATTRIBUTES = new Set(["href", "src", "alt"]);
|
|
49
52
|
|
|
50
|
-
/** Field shape {@link wrapEditableValues} needs: name + kind. */
|
|
53
|
+
/** Field shape {@link wrapEditableValues} needs: name + kind (+ a list field's type and binding). */
|
|
51
54
|
export interface EditableFieldDef {
|
|
52
55
|
readonly name: string;
|
|
53
56
|
readonly kind?: string;
|
|
57
|
+
readonly listType?: string;
|
|
58
|
+
readonly bind?: string;
|
|
54
59
|
}
|
|
55
60
|
|
|
61
|
+
/** List type lookup by slug, for marking a local list's entries. */
|
|
62
|
+
export type EditableListTypes = (slug: string) => ListTypeDef | undefined;
|
|
63
|
+
|
|
64
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
65
|
+
|
|
56
66
|
function editable(value: unknown): value is string {
|
|
57
67
|
return typeof value === "string" && value.trim().length > 0 && !value.includes("<");
|
|
58
68
|
}
|
|
@@ -65,9 +75,12 @@ function wrap(path: string, value: string): string {
|
|
|
65
75
|
* Wrap the editable text of a content instance's values in sentinels.
|
|
66
76
|
* Returns a new object; non-editable values pass through untouched.
|
|
67
77
|
*
|
|
68
|
-
* Paths: `<field>` for a `text` / `url` / `image` field
|
|
69
|
-
* for a `text
|
|
70
|
-
* `link
|
|
78
|
+
* Paths: `<field>` for a `text` / `url` / `image` field;
|
|
79
|
+
* `<field>.<itemId>.<key>` for a `text` value of a local list entry and
|
|
80
|
+
* `<field>.<itemId>.<key>.href` for the target of its `link` value (item
|
|
81
|
+
* ids are stable uuids, so a path survives reordering). Shared lists are
|
|
82
|
+
* edited in the list editor, not in place: their values are not part of
|
|
83
|
+
* the placement.
|
|
71
84
|
*
|
|
72
85
|
* @example
|
|
73
86
|
* wrapEditableValues({ hero_title: "Hi" }, [{ name: "hero_title", kind: "text" }]);
|
|
@@ -76,6 +89,7 @@ function wrap(path: string, value: string): string {
|
|
|
76
89
|
export function wrapEditableValues(
|
|
77
90
|
values: Readonly<Record<string, unknown>>,
|
|
78
91
|
fields: readonly EditableFieldDef[],
|
|
92
|
+
listTypes?: EditableListTypes,
|
|
79
93
|
): Record<string, unknown> {
|
|
80
94
|
const out: Record<string, unknown> = { ...values };
|
|
81
95
|
for (const f of fields) {
|
|
@@ -84,23 +98,41 @@ export function wrapEditableValues(
|
|
|
84
98
|
const v = values[f.name];
|
|
85
99
|
if (kind === "text" || kind === "url" || kind === "image") {
|
|
86
100
|
if (editable(v)) out[f.name] = wrap(f.name, v);
|
|
87
|
-
} else if (kind === "
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
out[f.name] = v.map((item, i) => {
|
|
91
|
-
if (typeof item !== "object" || item === null) return item;
|
|
92
|
-
const { label, href } = item as { label?: unknown; href?: unknown };
|
|
93
|
-
return {
|
|
94
|
-
...item,
|
|
95
|
-
...(editable(label) ? { label: wrap(`${f.name}.${i}.label`, label) } : {}),
|
|
96
|
-
...(editable(href) ? { href: wrap(`${f.name}.${i}.href`, href) } : {}),
|
|
97
|
-
};
|
|
98
|
-
});
|
|
101
|
+
} else if (kind === "list" && (f.bind ?? "local") === "local" && Array.isArray(v)) {
|
|
102
|
+
const type = f.listType === undefined ? undefined : listTypes?.(f.listType);
|
|
103
|
+
if (type) out[f.name] = wrapListEntries(f.name, v, type);
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
return out;
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
/** Mark the text and link targets of local list entries, at any depth. */
|
|
110
|
+
function wrapListEntries(field: string, items: readonly unknown[], type: ListTypeDef): unknown[] {
|
|
111
|
+
return items.map((raw) => {
|
|
112
|
+
if (typeof raw !== "object" || raw === null) return raw;
|
|
113
|
+
const item = raw as { id?: unknown; node?: unknown; values?: unknown; children?: unknown };
|
|
114
|
+
const node = typeof item.node === "string" ? type.nodes[item.node] : undefined;
|
|
115
|
+
if (!node || typeof item.id !== "string" || !UUID_RE.test(item.id)) return raw;
|
|
116
|
+
const values = { ...((item.values ?? {}) as Record<string, unknown>) };
|
|
117
|
+
for (const def of node.fields) {
|
|
118
|
+
const v = values[def.name];
|
|
119
|
+
const at = `${field}.${item.id}.${def.name}`;
|
|
120
|
+
if (def.type === "text" && editable(v)) values[def.name] = wrap(at, v);
|
|
121
|
+
if (def.type === "link" && typeof v === "object" && v !== null) {
|
|
122
|
+
const href = (v as { href?: unknown }).href;
|
|
123
|
+
if (editable(href)) values[def.name] = { ...v, href: wrap(`${at}.href`, href) };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
...item,
|
|
128
|
+
values,
|
|
129
|
+
...(Array.isArray(item.children)
|
|
130
|
+
? { children: wrapListEntries(field, item.children, type) }
|
|
131
|
+
: {}),
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
104
136
|
/** Comment-safe encoding: no `--`, no `>`, no whitespace. */
|
|
105
137
|
function attr(value: string): string {
|
|
106
138
|
return encodeURIComponent(value).replace(/-/g, "%2D");
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MPL-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Source text whose line breaks arrived string-escaped as a whole.
|
|
5
|
+
*
|
|
6
|
+
* e2e-livedit run 40532297620 (release gate v0.11.1): `add_module`'s html,
|
|
7
|
+
* css and js came in with every line break as the two characters `\` `n`
|
|
8
|
+
* (`(function () {\n document…`). Stored as sent, the footer showed `\n`
|
|
9
|
+
* text, its CSS broke, and the page's shared module `<script>` threw
|
|
10
|
+
* `SyntaxError: Invalid or unexpected token` — a backslash outside a string
|
|
11
|
+
* is not JavaScript. Like the other whole-value transport encodings
|
|
12
|
+
* (`source-text-transport.ts`) the AI tool boundary decodes it, once.
|
|
13
|
+
*
|
|
14
|
+
* The same two characters are legitimate inside a string literal (`"a\nb"`
|
|
15
|
+
* in JS or a CSS `content`), a regex, an HTML attribute value, or the body
|
|
16
|
+
* of a raw-text element. A value is only decoded when EVERY escaped line
|
|
17
|
+
* break sits where a real line break could have stood — between tokens of
|
|
18
|
+
* code, or in markup text between tags. One escape inside a literal makes
|
|
19
|
+
* the value ambiguous and it is left exactly as sent: decoding it would
|
|
20
|
+
* turn valid source into broken source (a raw newline inside a JS string
|
|
21
|
+
* is a syntax error). The op validators still refuse what is left broken
|
|
22
|
+
* (module js is parsed in admin-core `module-js-syntax.ts`).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/** Which grammar a source value is written in; decides what counts as a literal. */
|
|
26
|
+
export type SourceLanguage = "html" | "css" | "js";
|
|
27
|
+
|
|
28
|
+
/** `\n` or `\r\n` escape starting at `i` (a single backslash, not an escaped one). */
|
|
29
|
+
function escapedLineBreakLength(value: string, i: number): number {
|
|
30
|
+
if (value[i] !== "\\") return 0;
|
|
31
|
+
if (value[i + 1] === "n") return 2;
|
|
32
|
+
if (value[i + 1] === "r" && value[i + 2] === "\\" && value[i + 3] === "n") return 4;
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Characters after which a `/` in JS starts a regex literal rather than a division. */
|
|
37
|
+
const REGEX_PRECEDERS = new Set([..."(,=:[!&|?{};+-*%<>~^"]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Index just past a quoted literal starting at `start` (its quote char), or
|
|
41
|
+
* -1 when an escaped line break sits inside it.
|
|
42
|
+
*/
|
|
43
|
+
function skipQuoted(value: string, start: number): number {
|
|
44
|
+
const quote = value[start];
|
|
45
|
+
let j = start + 1;
|
|
46
|
+
while (j < value.length) {
|
|
47
|
+
const ch = value[j];
|
|
48
|
+
if (ch === "\\") {
|
|
49
|
+
if (value[j + 1] === "n" || value[j + 1] === "r") return -1;
|
|
50
|
+
j += 2;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (ch === quote) return j + 1;
|
|
54
|
+
j += 1;
|
|
55
|
+
}
|
|
56
|
+
return j;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True when an escaped line break sits inside a JS string, template, or regex literal. */
|
|
60
|
+
function jsLiteralHoldsEscape(value: string): boolean {
|
|
61
|
+
let prev = "";
|
|
62
|
+
let i = 0;
|
|
63
|
+
while (i < value.length) {
|
|
64
|
+
const ch = value[i] as string;
|
|
65
|
+
const lineBreak = escapedLineBreakLength(value, i);
|
|
66
|
+
if (lineBreak > 0) {
|
|
67
|
+
prev = "";
|
|
68
|
+
i += lineBreak;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (ch === "\\") {
|
|
72
|
+
i += 2;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (ch === '"' || ch === "'" || ch === "`") {
|
|
76
|
+
const end = skipQuoted(value, i);
|
|
77
|
+
if (end < 0) return true;
|
|
78
|
+
prev = ch;
|
|
79
|
+
i = end;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (ch === "/" && value[i + 1] === "/") {
|
|
83
|
+
// A line comment runs to the next (escaped) line break.
|
|
84
|
+
i += 2;
|
|
85
|
+
while (i < value.length && escapedLineBreakLength(value, i) === 0) i += 1;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (ch === "/" && value[i + 1] === "*") {
|
|
89
|
+
const close = value.indexOf("*/", i + 2);
|
|
90
|
+
i = close < 0 ? value.length : close + 2;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (ch === "/" && (prev === "" || REGEX_PRECEDERS.has(prev))) {
|
|
94
|
+
let j = i + 1;
|
|
95
|
+
let inClass = false;
|
|
96
|
+
while (j < value.length) {
|
|
97
|
+
const r = value[j];
|
|
98
|
+
if (r === "\\") {
|
|
99
|
+
if (value[j + 1] === "n" || value[j + 1] === "r") return true;
|
|
100
|
+
j += 2;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (r === "[") inClass = true;
|
|
104
|
+
else if (r === "]") inClass = false;
|
|
105
|
+
else if (r === "/" && !inClass) break;
|
|
106
|
+
j += 1;
|
|
107
|
+
}
|
|
108
|
+
prev = "/";
|
|
109
|
+
i = j + 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (ch !== " " && ch !== "\t") prev = ch;
|
|
113
|
+
i += 1;
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** True when an escaped line break sits inside a CSS string. */
|
|
119
|
+
function cssLiteralHoldsEscape(value: string): boolean {
|
|
120
|
+
let i = 0;
|
|
121
|
+
while (i < value.length) {
|
|
122
|
+
const ch = value[i];
|
|
123
|
+
if (ch === "/" && value[i + 1] === "*") {
|
|
124
|
+
const close = value.indexOf("*/", i + 2);
|
|
125
|
+
i = close < 0 ? value.length : close + 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (ch === '"' || ch === "'") {
|
|
129
|
+
const end = skipQuoted(value, i);
|
|
130
|
+
if (end < 0) return true;
|
|
131
|
+
i = end;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
i += ch === "\\" ? 2 : 1;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Elements whose body is not markup text: an escape there belongs to that body. */
|
|
140
|
+
const RAW_TEXT_ELEMENTS = new Set(["script", "style", "pre", "textarea"]);
|
|
141
|
+
|
|
142
|
+
/** True when an escaped line break sits inside an attribute value, a comment, or a raw-text element body. */
|
|
143
|
+
function htmlLiteralHoldsEscape(value: string): boolean {
|
|
144
|
+
const lower = value.toLowerCase();
|
|
145
|
+
let i = 0;
|
|
146
|
+
while (i < value.length) {
|
|
147
|
+
if (value[i] !== "<") {
|
|
148
|
+
i += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (value.startsWith("<!--", i)) {
|
|
152
|
+
const close = value.indexOf("-->", i + 4);
|
|
153
|
+
const end = close < 0 ? value.length : close + 3;
|
|
154
|
+
if (/(?<!\\)\\[nr]/.test(value.slice(i, end))) return true;
|
|
155
|
+
i = end;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const name = /^<([a-zA-Z][a-zA-Z0-9-]*)/.exec(value.slice(i, i + 64))?.[1]?.toLowerCase();
|
|
159
|
+
let j = i + 1;
|
|
160
|
+
while (j < value.length && value[j] !== ">") {
|
|
161
|
+
if (value[j] === '"' || value[j] === "'") {
|
|
162
|
+
const end = skipQuoted(value, j);
|
|
163
|
+
if (end < 0) return true;
|
|
164
|
+
j = end;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
j += 1;
|
|
168
|
+
}
|
|
169
|
+
i = j + 1;
|
|
170
|
+
if (name && RAW_TEXT_ELEMENTS.has(name)) {
|
|
171
|
+
const close = lower.indexOf(`</${name}`, i);
|
|
172
|
+
const end = close < 0 ? value.length : close;
|
|
173
|
+
if (/(?<!\\)\\[nr]/.test(value.slice(i, end))) return true;
|
|
174
|
+
i = end;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const LITERAL_HOLDS_ESCAPE: Readonly<Record<SourceLanguage, (value: string) => boolean>> = {
|
|
181
|
+
html: htmlLiteralHoldsEscape,
|
|
182
|
+
css: cssLiteralHoldsEscape,
|
|
183
|
+
js: jsLiteralHoldsEscape,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/** An escaped line break (`\n`, `\r\n`) not preceded by another backslash. */
|
|
187
|
+
const ESCAPED_LINE_BREAK = /(?<!\\)(?:\\r)?\\n/g;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* True when a source value's line breaks were string-escaped as a whole:
|
|
191
|
+
* no real line break at all, at least two escaped ones, one of them
|
|
192
|
+
* followed by indentation (the signature of pretty-printed source), and
|
|
193
|
+
* none inside a literal of `language` (see the file comment).
|
|
194
|
+
*
|
|
195
|
+
* @param value - the source text as sent.
|
|
196
|
+
* @param language - the grammar the value is written in.
|
|
197
|
+
*/
|
|
198
|
+
export function hasEscapedLineBreaks(value: string, language: SourceLanguage): boolean {
|
|
199
|
+
if (/[\r\n]/.test(value)) return false;
|
|
200
|
+
const hits = value.match(ESCAPED_LINE_BREAK)?.length ?? 0;
|
|
201
|
+
if (hits < 2) return false;
|
|
202
|
+
if (!/(?<!\\)\\n(?:[ \t]|\\t)/.test(value)) return false;
|
|
203
|
+
return !LITERAL_HOLDS_ESCAPE[language](value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The value with every escaped line break turned back into a real one (and
|
|
208
|
+
* the escaped tabs indenting the next line into real tabs) when
|
|
209
|
+
* `hasEscapedLineBreaks` holds; the input unchanged otherwise. Nothing else
|
|
210
|
+
* is unescaped: quotes and backslashes arrived as they were meant.
|
|
211
|
+
*
|
|
212
|
+
* @param value - the tool argument (non-strings pass through).
|
|
213
|
+
* @param language - the grammar the value is written in.
|
|
214
|
+
*/
|
|
215
|
+
export function decodeEscapedLineBreaks(value: unknown, language: SourceLanguage): unknown {
|
|
216
|
+
if (typeof value !== "string" || !hasEscapedLineBreaks(value, language)) return value;
|
|
217
|
+
return value.replace(
|
|
218
|
+
/(?<!\\)(?:\\r)?\\n((?:\\t)*)/g,
|
|
219
|
+
(_m, tabs: string) => `\n${"\t".repeat(tabs.length / 2)}`,
|
|
220
|
+
);
|
|
221
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,11 +14,13 @@ export * from "./content.js";
|
|
|
14
14
|
export * from "./context.js";
|
|
15
15
|
export * from "./css-gradient-scan.js";
|
|
16
16
|
export * from "./css-scope-scan.js";
|
|
17
|
+
export * from "./css-var-aliases.js";
|
|
17
18
|
export * from "./css-var-scan.js";
|
|
18
19
|
export * from "./database-url.js";
|
|
19
20
|
export * from "./design-draft-shell.js";
|
|
20
21
|
export * from "./document-language.js";
|
|
21
22
|
export * from "./edit-markers.js";
|
|
23
|
+
export * from "./escaped-line-breaks.js";
|
|
22
24
|
export * from "./font-assets.js";
|
|
23
25
|
export * from "./font-axis-policy.js";
|
|
24
26
|
export * from "./font-usage.js";
|
|
@@ -32,6 +34,7 @@ export * from "./image-capabilities.js";
|
|
|
32
34
|
export * from "./interactions.js";
|
|
33
35
|
export * from "./link-canonicalise.js";
|
|
34
36
|
export * from "./link-refs.js";
|
|
37
|
+
export * from "./lists/index.js";
|
|
35
38
|
export {
|
|
36
39
|
type LogContext,
|
|
37
40
|
type Logger,
|
|
@@ -72,11 +75,11 @@ export * from "./screenshot-capture.js";
|
|
|
72
75
|
export * from "./seo.js";
|
|
73
76
|
export * from "./skills.js";
|
|
74
77
|
export * from "./snapshots.js";
|
|
78
|
+
export * from "./source-text-transport.js";
|
|
75
79
|
export * from "./staging-gate.js";
|
|
76
80
|
export * from "./static-cache-policy.js";
|
|
77
81
|
export * from "./static-security-headers.js";
|
|
78
82
|
export * from "./strip-cdata.js";
|
|
79
|
-
export * from "./structured-sets.js";
|
|
80
83
|
export * from "./subagents.js";
|
|
81
84
|
export * from "./template-engine.js";
|
|
82
85
|
export * from "./theme-importers/auto-detect.js";
|