meno-core 1.0.52 → 1.0.53
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/build-astro.ts +183 -13
- package/build-next.ts +1361 -0
- package/build-static.ts +7 -5
- package/dist/bin/cli.js +2 -2
- package/dist/build-static.js +6 -6
- package/dist/chunks/{chunk-HNLUO36W.js → chunk-GZHGVVW3.js} +2 -2
- package/dist/chunks/chunk-GZHGVVW3.js.map +7 -0
- package/dist/chunks/{chunk-LPVETICS.js → chunk-H3GJ4H2U.js} +185 -1
- package/dist/chunks/chunk-H3GJ4H2U.js.map +7 -0
- package/dist/chunks/{chunk-CXCBV2M7.js → chunk-IGYR22T6.js} +76 -270
- package/dist/chunks/chunk-IGYR22T6.js.map +7 -0
- package/dist/chunks/{chunk-LHLHPYSP.js → chunk-JGP5A3Y5.js} +12 -11
- package/dist/chunks/chunk-JGP5A3Y5.js.map +7 -0
- package/dist/chunks/{chunk-7NIC4I3V.js → chunk-JGWFTO6P.js} +167 -21
- package/dist/chunks/chunk-JGWFTO6P.js.map +7 -0
- package/dist/chunks/{chunk-EDQSMAMP.js → chunk-O3NAGJP4.js} +85 -4
- package/dist/chunks/chunk-O3NAGJP4.js.map +7 -0
- package/dist/chunks/{chunk-H4JSCDNW.js → chunk-QB2LNO4W.js} +24 -1
- package/dist/chunks/chunk-QB2LNO4W.js.map +7 -0
- package/dist/chunks/{chunk-A725KYFK.js → chunk-R6XHAFBF.js} +561 -112
- package/dist/chunks/chunk-R6XHAFBF.js.map +7 -0
- package/dist/chunks/{chunk-J23ZX5AP.js → chunk-X754AHS5.js} +277 -1
- package/dist/chunks/chunk-X754AHS5.js.map +7 -0
- package/dist/chunks/{chunk-2QK6U5UK.js → chunk-YBLHKYFF.js} +12 -2
- package/dist/chunks/chunk-YBLHKYFF.js.map +7 -0
- package/dist/chunks/{constants-GWBAD66U.js → constants-STK2YBIW.js} +2 -2
- package/dist/entries/server-router.js +7 -7
- package/dist/lib/client/index.js +354 -59
- package/dist/lib/client/index.js.map +4 -4
- package/dist/lib/server/index.js +1458 -190
- package/dist/lib/server/index.js.map +4 -4
- package/dist/lib/shared/index.js +202 -34
- package/dist/lib/shared/index.js.map +4 -4
- package/dist/lib/test-utils/index.js +1 -1
- package/entries/client-router.tsx +5 -165
- package/lib/client/ErrorBoundary.test.tsx +27 -25
- package/lib/client/ErrorBoundary.tsx +34 -19
- package/lib/client/core/ComponentBuilder.ts +19 -2
- package/lib/client/core/builders/embedBuilder.ts +8 -4
- package/lib/client/core/builders/listBuilder.ts +23 -4
- package/lib/client/fontFamiliesService.test.ts +76 -0
- package/lib/client/fontFamiliesService.ts +69 -0
- package/lib/client/hmrCssReload.ts +160 -0
- package/lib/client/hooks/useColorVariables.ts +2 -0
- package/lib/client/index.ts +4 -0
- package/lib/client/meno-filter/ui.ts +2 -0
- package/lib/client/routing/RouteLoader.test.ts +2 -2
- package/lib/client/routing/RouteLoader.ts +8 -2
- package/lib/client/routing/Router.tsx +81 -15
- package/lib/client/scripts/ScriptExecutor.test.ts +143 -0
- package/lib/client/scripts/ScriptExecutor.ts +56 -2
- package/lib/client/styles/StyleInjector.ts +20 -5
- package/lib/client/styles/UtilityClassCollector.ts +7 -1
- package/lib/client/styles/cspNonce.test.ts +67 -0
- package/lib/client/styles/cspNonce.ts +63 -0
- package/lib/client/templateEngine.test.ts +80 -0
- package/lib/client/templateEngine.ts +5 -0
- package/lib/server/astro/cmsPageEmitter.ts +35 -5
- package/lib/server/astro/componentEmitter.ts +61 -5
- package/lib/server/astro/nodeToAstro.ts +149 -11
- package/lib/server/astro/normalizeOrphanTemplateProps.test.ts +264 -0
- package/lib/server/astro/normalizeOrphanTemplateProps.ts +184 -0
- package/lib/server/createServer.ts +11 -0
- package/lib/server/draftPageStore.ts +49 -0
- package/lib/server/fileWatcher.ts +62 -2
- package/lib/server/index.ts +13 -1
- package/lib/server/providers/fileSystemPageProvider.ts +8 -0
- package/lib/server/routes/api/components.ts +9 -4
- package/lib/server/routes/api/core-routes.ts +2 -2
- package/lib/server/routes/api/pages.ts +14 -22
- package/lib/server/routes/api/shared.ts +56 -0
- package/lib/server/routes/index.ts +90 -0
- package/lib/server/routes/pages.ts +13 -6
- package/lib/server/services/componentService.test.ts +199 -2
- package/lib/server/services/componentService.ts +354 -49
- package/lib/server/services/fileWatcherService.ts +4 -24
- package/lib/server/services/pageService.test.ts +23 -0
- package/lib/server/services/pageService.ts +124 -6
- package/lib/server/ssr/attributeBuilder.ts +8 -2
- package/lib/server/ssr/buildErrorOverlay.ts +1 -1
- package/lib/server/ssr/errorOverlay.test.ts +21 -2
- package/lib/server/ssr/errorOverlay.ts +38 -11
- package/lib/server/ssr/htmlGenerator.test.ts +53 -13
- package/lib/server/ssr/htmlGenerator.ts +71 -27
- package/lib/server/ssr/liveReloadIntegration.test.ts +123 -2
- package/lib/server/ssr/ssrRenderer.test.ts +67 -0
- package/lib/server/ssr/ssrRenderer.ts +94 -9
- package/lib/server/websocketManager.ts +0 -1
- package/lib/shared/componentRefs.ts +45 -0
- package/lib/shared/constants.ts +8 -0
- package/lib/shared/cssGeneration.ts +2 -0
- package/lib/shared/cssProperties.ts +184 -0
- package/lib/shared/expressionEvaluator.ts +54 -0
- package/lib/shared/fontCss.ts +101 -0
- package/lib/shared/fontLoader.ts +8 -86
- package/lib/shared/friendlyError.test.ts +87 -0
- package/lib/shared/friendlyError.ts +121 -0
- package/lib/shared/hrefRefs.test.ts +130 -0
- package/lib/shared/hrefRefs.ts +100 -0
- package/lib/shared/index.ts +52 -0
- package/lib/shared/inlineSvgStyleRules.test.ts +108 -0
- package/lib/shared/inlineSvgStyleRules.ts +134 -0
- package/lib/shared/interfaces/contentProvider.ts +13 -0
- package/lib/shared/itemTemplateUtils.test.ts +14 -0
- package/lib/shared/itemTemplateUtils.ts +4 -1
- package/lib/shared/registry/NodeTypeDefinition.ts +1 -1
- package/lib/shared/registry/nodeTypes/LinkNodeType.ts +1 -1
- package/lib/shared/slugTranslator.test.ts +24 -0
- package/lib/shared/slugTranslator.ts +24 -0
- package/lib/shared/styleNodeUtils.ts +4 -1
- package/lib/shared/tree/PathBuilder.test.ts +128 -1
- package/lib/shared/tree/PathBuilder.ts +83 -31
- package/lib/shared/types/comment.ts +99 -0
- package/lib/shared/types/index.ts +12 -0
- package/lib/shared/types/rendering.ts +8 -0
- package/lib/shared/utilityClassConfig.ts +4 -2
- package/lib/shared/utilityClassMapper.test.ts +24 -0
- package/lib/shared/validation/commentValidators.ts +69 -0
- package/lib/shared/validation/index.ts +1 -0
- package/lib/shared/viewportUnits.integration.test.ts +42 -0
- package/lib/shared/viewportUnits.test.ts +103 -0
- package/lib/shared/viewportUnits.ts +63 -0
- package/lib/test-utils/dom-setup.ts +6 -0
- package/package.json +1 -1
- package/dist/chunks/chunk-2QK6U5UK.js.map +0 -7
- package/dist/chunks/chunk-7NIC4I3V.js.map +0 -7
- package/dist/chunks/chunk-A725KYFK.js.map +0 -7
- package/dist/chunks/chunk-CXCBV2M7.js.map +0 -7
- package/dist/chunks/chunk-EDQSMAMP.js.map +0 -7
- package/dist/chunks/chunk-H4JSCDNW.js.map +0 -7
- package/dist/chunks/chunk-HNLUO36W.js.map +0 -7
- package/dist/chunks/chunk-J23ZX5AP.js.map +0 -7
- package/dist/chunks/chunk-LHLHPYSP.js.map +0 -7
- package/dist/chunks/chunk-LPVETICS.js.map +0 -7
- /package/dist/chunks/{constants-GWBAD66U.js.map → constants-STK2YBIW.js.map} +0 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Friendly error mapping.
|
|
3
|
+
*
|
|
4
|
+
* Turns a raw JavaScript error (the kind that leaks scary text like
|
|
5
|
+
* "Cannot read properties of null (reading 'length')") into plain-language
|
|
6
|
+
* copy suitable for non-technical users, while always preserving the original
|
|
7
|
+
* message in `raw` so developers can still copy the exact text for debugging.
|
|
8
|
+
*
|
|
9
|
+
* Pure and dependency-free so it can be imported from core/server (SSR error
|
|
10
|
+
* overlay), core/client (ErrorBoundary), and @meno/studio (the editor toast).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface FriendlyError {
|
|
14
|
+
/** Short headline, e.g. "A section couldn't load its content". */
|
|
15
|
+
title: string;
|
|
16
|
+
/** Plain-language explanation of what went wrong. */
|
|
17
|
+
friendlyMessage: string;
|
|
18
|
+
/** Optional actionable suggestion. */
|
|
19
|
+
hint?: string;
|
|
20
|
+
/** Original error message — ALWAYS preserved for debugging / copy. */
|
|
21
|
+
raw: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Extract the raw message string from any thrown value. */
|
|
25
|
+
function rawMessageOf(input: unknown): string {
|
|
26
|
+
if (input instanceof Error) return input.message;
|
|
27
|
+
if (typeof input === 'string') return input;
|
|
28
|
+
if (input && typeof input === 'object' && 'message' in input) {
|
|
29
|
+
const m = (input as { message: unknown }).message;
|
|
30
|
+
if (typeof m === 'string') return m;
|
|
31
|
+
}
|
|
32
|
+
return String(input);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Rule {
|
|
36
|
+
test: RegExp;
|
|
37
|
+
build: (m: RegExpMatchArray, raw: string) => Omit<FriendlyError, 'raw'>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Matched in order; first hit wins. Keep the most specific patterns first.
|
|
41
|
+
const RULES: Rule[] = [
|
|
42
|
+
{
|
|
43
|
+
// Modern V8: "Cannot read properties of null (reading 'length')".
|
|
44
|
+
// Legacy V8: "Cannot read property 'length' of undefined".
|
|
45
|
+
test: /Cannot read propert(?:y|ies) (?:of (?:null|undefined)(?: \(reading '([^']+)'\))?|'([^']+)' of (?:null|undefined))/i,
|
|
46
|
+
build: (m) => {
|
|
47
|
+
const prop = m[1] || m[2];
|
|
48
|
+
const target = prop ? `(\`${prop}\`)` : '';
|
|
49
|
+
return {
|
|
50
|
+
title: "A section couldn't load its content",
|
|
51
|
+
friendlyMessage:
|
|
52
|
+
`Something this section expected ${target} wasn't available — ` +
|
|
53
|
+
`often a list or field that's empty or not connected yet.`,
|
|
54
|
+
hint: 'Check the data source or list binding for this section.',
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
test: /(\S+) is not a function/i,
|
|
60
|
+
build: (m) => ({
|
|
61
|
+
title: "A piece of code didn't run as expected",
|
|
62
|
+
friendlyMessage: `The site tried to use \`${m[1]}\` as a function, but it isn't one.`,
|
|
63
|
+
hint: "Check the component's custom JavaScript.",
|
|
64
|
+
}),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
test: /(\S+) is not defined/i,
|
|
68
|
+
build: (m) => ({
|
|
69
|
+
title: 'A piece of code referenced something missing',
|
|
70
|
+
friendlyMessage: `\`${m[1]}\` was used but never defined.`,
|
|
71
|
+
hint: 'Check for a typo or a missing import in custom code.',
|
|
72
|
+
}),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
test: /Failed to fetch|NetworkError|network request failed/i,
|
|
76
|
+
build: () => ({
|
|
77
|
+
title: "Couldn't reach the network",
|
|
78
|
+
friendlyMessage: 'A request to load data failed.',
|
|
79
|
+
hint: 'Check your connection or the endpoint URL.',
|
|
80
|
+
}),
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
test: /Cannot find module|Failed to resolve module|Failed to load module|Error loading dynamically imported module/i,
|
|
84
|
+
build: () => ({
|
|
85
|
+
title: "A required file couldn't be loaded",
|
|
86
|
+
friendlyMessage: 'An import or module reference couldn\'t be found.',
|
|
87
|
+
hint: 'Check the import path.',
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
test: /Unexpected token|Unexpected end of (?:JSON|input)|in JSON at position|is not valid JSON/i,
|
|
92
|
+
build: () => ({
|
|
93
|
+
title: 'Some data was malformed',
|
|
94
|
+
friendlyMessage: "Content that should be valid JSON couldn't be parsed.",
|
|
95
|
+
hint: 'Check the JSON for a syntax error.',
|
|
96
|
+
}),
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Convert any error-like value into a friendly, user-presentable shape.
|
|
102
|
+
* Unknown errors fall back to a reassuring generic message; `raw` always
|
|
103
|
+
* carries the original text.
|
|
104
|
+
*/
|
|
105
|
+
export function toFriendlyError(input: unknown): FriendlyError {
|
|
106
|
+
const raw = rawMessageOf(input);
|
|
107
|
+
|
|
108
|
+
for (const rule of RULES) {
|
|
109
|
+
const match = raw.match(rule.test);
|
|
110
|
+
if (match) {
|
|
111
|
+
return { ...rule.build(match, raw), raw };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
title: 'This section ran into a problem',
|
|
117
|
+
friendlyMessage:
|
|
118
|
+
'Something went wrong while rendering this part of the page. The rest of the page is fine.',
|
|
119
|
+
raw,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { rewriteHrefRefs, countHrefRefs } from './hrefRefs';
|
|
3
|
+
|
|
4
|
+
describe('rewriteHrefRefs', () => {
|
|
5
|
+
test('rewrites exact match on a link node', () => {
|
|
6
|
+
const node = { type: 'link', href: '/about', children: [] };
|
|
7
|
+
expect(rewriteHrefRefs(node, '/about', '/about-us')).toBe(true);
|
|
8
|
+
expect(node.href).toBe('/about-us');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('rewrites prefix match with trailing slash (sub-path)', () => {
|
|
12
|
+
const node = { type: 'link', href: '/blog/post-1' };
|
|
13
|
+
expect(rewriteHrefRefs(node, '/blog', '/news')).toBe(true);
|
|
14
|
+
expect(node.href).toBe('/news/post-1');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('rewrites prefix match with query string', () => {
|
|
18
|
+
const node = { type: 'link', href: '/about?ref=footer' };
|
|
19
|
+
expect(rewriteHrefRefs(node, '/about', '/about-us')).toBe(true);
|
|
20
|
+
expect(node.href).toBe('/about-us?ref=footer');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('rewrites prefix match with hash fragment', () => {
|
|
24
|
+
const node = { type: 'link', href: '/about#team' };
|
|
25
|
+
expect(rewriteHrefRefs(node, '/about', '/about-us')).toBe(true);
|
|
26
|
+
expect(node.href).toBe('/about-us#team');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('does not match unrelated paths that share a prefix substring', () => {
|
|
30
|
+
// "/about-us" must NOT match when rewriting "/about" → "/contact",
|
|
31
|
+
// because there's no path-segment boundary at the match point.
|
|
32
|
+
const node = { type: 'link', href: '/about-us' };
|
|
33
|
+
expect(rewriteHrefRefs(node, '/about', '/contact')).toBe(false);
|
|
34
|
+
expect(node.href).toBe('/about-us');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('does not touch external URLs', () => {
|
|
38
|
+
const node = { type: 'link', href: 'https://example.com/about' };
|
|
39
|
+
expect(rewriteHrefRefs(node, '/about', '/about-us')).toBe(false);
|
|
40
|
+
expect(node.href).toBe('https://example.com/about');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('skips non-string href values (LinkMapping)', () => {
|
|
44
|
+
const node = { type: 'link', href: { source: 'cms', field: 'url' } };
|
|
45
|
+
expect(rewriteHrefRefs(node, '/about', '/about-us')).toBe(false);
|
|
46
|
+
expect(node.href).toEqual({ source: 'cms', field: 'url' });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('walks children arrays recursively', () => {
|
|
50
|
+
const tree = {
|
|
51
|
+
type: 'node',
|
|
52
|
+
tag: 'div',
|
|
53
|
+
children: [
|
|
54
|
+
{ type: 'link', href: '/about' },
|
|
55
|
+
{ type: 'node', tag: 'span', children: [{ type: 'link', href: '/about/team' }] },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
expect(rewriteHrefRefs(tree, '/about', '/about-us')).toBe(true);
|
|
59
|
+
expect(tree.children[0].href).toBe('/about-us');
|
|
60
|
+
expect((tree.children[1].children![0] as any).href).toBe('/about-us/team');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('walks prop trees (component nodes with object props)', () => {
|
|
64
|
+
const tree = {
|
|
65
|
+
type: 'component',
|
|
66
|
+
component: 'Hero',
|
|
67
|
+
props: {
|
|
68
|
+
cta: { href: '/about', label: 'Learn more' },
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
expect(rewriteHrefRefs(tree, '/about', '/about-us')).toBe(true);
|
|
72
|
+
expect((tree as any).props.cta.href).toBe('/about-us');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('returns false when no oldHref provided', () => {
|
|
76
|
+
const node = { type: 'link', href: '/about' };
|
|
77
|
+
expect(rewriteHrefRefs(node, '', '/anything')).toBe(false);
|
|
78
|
+
expect(rewriteHrefRefs(node, '/about', '/about')).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('rewrites every matching href and reports true if any changed', () => {
|
|
82
|
+
const tree = {
|
|
83
|
+
type: 'node',
|
|
84
|
+
children: [
|
|
85
|
+
{ type: 'link', href: '/about' },
|
|
86
|
+
{ type: 'link', href: '/contact' },
|
|
87
|
+
{ type: 'link', href: '/about?utm=x' },
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
expect(rewriteHrefRefs(tree, '/about', '/about-us')).toBe(true);
|
|
91
|
+
expect((tree.children[0] as any).href).toBe('/about-us');
|
|
92
|
+
expect((tree.children[1] as any).href).toBe('/contact');
|
|
93
|
+
expect((tree.children[2] as any).href).toBe('/about-us?utm=x');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('handles null / non-object inputs gracefully', () => {
|
|
97
|
+
expect(rewriteHrefRefs(null, '/a', '/b')).toBe(false);
|
|
98
|
+
expect(rewriteHrefRefs(undefined, '/a', '/b')).toBe(false);
|
|
99
|
+
expect(rewriteHrefRefs('string', '/a', '/b')).toBe(false);
|
|
100
|
+
expect(rewriteHrefRefs(42, '/a', '/b')).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('countHrefRefs', () => {
|
|
105
|
+
test('counts exact + prefix matches across nested nodes', () => {
|
|
106
|
+
const tree = {
|
|
107
|
+
type: 'node',
|
|
108
|
+
children: [
|
|
109
|
+
{ type: 'link', href: '/about' },
|
|
110
|
+
{ type: 'link', href: '/about/team' },
|
|
111
|
+
{ type: 'link', href: '/about?ref=x' },
|
|
112
|
+
{ type: 'link', href: '/about-us' }, // not a match
|
|
113
|
+
{ type: 'link', href: '/contact' },
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
expect(countHrefRefs(tree, '/about')).toBe(3);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('does not mutate the input tree', () => {
|
|
120
|
+
const tree = { type: 'link', href: '/about', children: [] };
|
|
121
|
+
const snapshot = JSON.stringify(tree);
|
|
122
|
+
countHrefRefs(tree, '/about');
|
|
123
|
+
expect(JSON.stringify(tree)).toBe(snapshot);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('returns 0 for empty href', () => {
|
|
127
|
+
const tree = { type: 'link', href: '/about' };
|
|
128
|
+
expect(countHrefRefs(tree, '')).toBe(0);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Href reference utilities — shared helpers for walking a node tree and
|
|
3
|
+
* rewriting `href` strings that point at an internal page.
|
|
4
|
+
*
|
|
5
|
+
* Used by the slug/page-rename flow on the server (cascade rewrite across
|
|
6
|
+
* pages + components) and unit-tested in isolation. Pure — no fs.
|
|
7
|
+
*
|
|
8
|
+
* Matching rules for a single href string `value` vs. `oldHref`:
|
|
9
|
+
* - exact match: `value === oldHref`
|
|
10
|
+
* - prefix match: `value` starts with `oldHref + "/"`, `oldHref + "?"`,
|
|
11
|
+
* or `oldHref + "#"` (catches deep links, query strings, fragments)
|
|
12
|
+
*
|
|
13
|
+
* Non-string href values (e.g. LinkMapping objects on a link node) are
|
|
14
|
+
* skipped — those reference scopes/CMS items, not page slugs.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function rewriteHref(value: string, oldHref: string, newHref: string): string | null {
|
|
18
|
+
if (value === oldHref) return newHref;
|
|
19
|
+
if (
|
|
20
|
+
value.startsWith(oldHref + '/') ||
|
|
21
|
+
value.startsWith(oldHref + '?') ||
|
|
22
|
+
value.startsWith(oldHref + '#')
|
|
23
|
+
) {
|
|
24
|
+
return newHref + value.substring(oldHref.length);
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Rewrite every `href: "<oldHref>"` (or `oldHref`-prefixed) string inside
|
|
31
|
+
* `node` to point at `newHref`. Returns true if any references were
|
|
32
|
+
* rewritten. Mutates `node` in place.
|
|
33
|
+
*
|
|
34
|
+
* Walks `children` recursively (array or single object) and any object-valued
|
|
35
|
+
* field on the node (covers component-prop-trees, nested structure, etc.).
|
|
36
|
+
*/
|
|
37
|
+
export function rewriteHrefRefs(
|
|
38
|
+
node: unknown,
|
|
39
|
+
oldHref: string,
|
|
40
|
+
newHref: string,
|
|
41
|
+
): boolean {
|
|
42
|
+
if (!node || typeof node !== 'object') return false;
|
|
43
|
+
if (!oldHref || oldHref === newHref) return false;
|
|
44
|
+
|
|
45
|
+
let changed = false;
|
|
46
|
+
const n = node as Record<string, unknown>;
|
|
47
|
+
|
|
48
|
+
if (typeof n.href === 'string') {
|
|
49
|
+
const rewritten = rewriteHref(n.href, oldHref, newHref);
|
|
50
|
+
if (rewritten !== null) {
|
|
51
|
+
n.href = rewritten;
|
|
52
|
+
changed = true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const key of Object.keys(n)) {
|
|
57
|
+
if (key === 'href') continue;
|
|
58
|
+
const v = n[key];
|
|
59
|
+
if (Array.isArray(v)) {
|
|
60
|
+
for (const item of v) {
|
|
61
|
+
if (item && typeof item === 'object') {
|
|
62
|
+
if (rewriteHrefRefs(item, oldHref, newHref)) changed = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} else if (v && typeof v === 'object') {
|
|
66
|
+
if (rewriteHrefRefs(v, oldHref, newHref)) changed = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return changed;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Count how many href references in `node` would be rewritten by
|
|
75
|
+
* rewriteHrefRefs(node, href, …). Does not mutate.
|
|
76
|
+
*/
|
|
77
|
+
export function countHrefRefs(node: unknown, href: string): number {
|
|
78
|
+
if (!node || typeof node !== 'object' || !href) return 0;
|
|
79
|
+
|
|
80
|
+
let count = 0;
|
|
81
|
+
const n = node as Record<string, unknown>;
|
|
82
|
+
|
|
83
|
+
if (typeof n.href === 'string' && rewriteHref(n.href, href, '__x__') !== null) {
|
|
84
|
+
count++;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const key of Object.keys(n)) {
|
|
88
|
+
if (key === 'href') continue;
|
|
89
|
+
const v = n[key];
|
|
90
|
+
if (Array.isArray(v)) {
|
|
91
|
+
for (const item of v) {
|
|
92
|
+
if (item && typeof item === 'object') count += countHrefRefs(item, href);
|
|
93
|
+
}
|
|
94
|
+
} else if (v && typeof v === 'object') {
|
|
95
|
+
count += countHrefRefs(v, href);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return count;
|
|
100
|
+
}
|
package/lib/shared/index.ts
CHANGED
|
@@ -22,6 +22,7 @@ export * from './breakpoints';
|
|
|
22
22
|
export * from './styleUtils';
|
|
23
23
|
export * from './cssProperties';
|
|
24
24
|
export * from './cssGeneration';
|
|
25
|
+
export * from './viewportUnits';
|
|
25
26
|
export * from './colorProperties';
|
|
26
27
|
export * from './cssNamedColors';
|
|
27
28
|
export * from './utilityClassMapper';
|
|
@@ -77,6 +78,14 @@ export {
|
|
|
77
78
|
validateCollectionId,
|
|
78
79
|
CMS_COLLECTION_ID_REGEX,
|
|
79
80
|
CMSCollectionIdSchema,
|
|
81
|
+
// Comment validators
|
|
82
|
+
CommentSchema,
|
|
83
|
+
CommentStatusSchema,
|
|
84
|
+
CommentAnchorSchema,
|
|
85
|
+
CommentAuthorSchema,
|
|
86
|
+
CommentThreadEntrySchema,
|
|
87
|
+
CommentNodeIdentitySchema,
|
|
88
|
+
validateComment,
|
|
80
89
|
} from './validation';
|
|
81
90
|
|
|
82
91
|
// Utils
|
|
@@ -100,6 +109,10 @@ export * from './responsiveScaling';
|
|
|
100
109
|
// Item template utilities (for CMS List)
|
|
101
110
|
export * from './itemTemplateUtils';
|
|
102
111
|
|
|
112
|
+
// Template-expression support predicate (mirrors expressionEvaluator's jsep grammar).
|
|
113
|
+
// The meno-astro codec uses this to tell a modelable `{{binding}}` from arbitrary JS.
|
|
114
|
+
export { isSupportedTemplateExpression } from './expressionEvaluator';
|
|
115
|
+
|
|
103
116
|
// Path security utilities
|
|
104
117
|
export * from './pathSecurity';
|
|
105
118
|
|
|
@@ -111,6 +124,7 @@ export * from './richtext';
|
|
|
111
124
|
|
|
112
125
|
// Error handling
|
|
113
126
|
export * from './errors';
|
|
127
|
+
export * from './friendlyError';
|
|
114
128
|
export { logRuntimeError, logNetworkError, setErrorHandler } from './errorLogger';
|
|
115
129
|
|
|
116
130
|
// Global template context
|
|
@@ -130,3 +144,41 @@ export * from './cmsQueryParser';
|
|
|
130
144
|
|
|
131
145
|
// Prop resolution utilities
|
|
132
146
|
export { resolvePropsFromDefinition, isRichTextMarker, richTextMarkerToHtml } from './propResolver';
|
|
147
|
+
|
|
148
|
+
// Component reference utilities (shared by rename API on server + editor sync on client)
|
|
149
|
+
export * from './componentRefs';
|
|
150
|
+
|
|
151
|
+
// Href reference utilities (shared by page-rename/slug-rename API cascade)
|
|
152
|
+
export * from './hrefRefs';
|
|
153
|
+
|
|
154
|
+
// Slug translation utilities (URL builder + slug index)
|
|
155
|
+
export {
|
|
156
|
+
buildPageUrlForLocale,
|
|
157
|
+
buildSlugIndex,
|
|
158
|
+
findPageBySlug,
|
|
159
|
+
getLocaleLinks,
|
|
160
|
+
resolveSlugToPageId,
|
|
161
|
+
translatePath,
|
|
162
|
+
} from './slugTranslator';
|
|
163
|
+
export type { LocaleLink, SlugMap } from './slugTranslator';
|
|
164
|
+
|
|
165
|
+
// Font CSS generation (pure: @font-face rules + preload tags from a fonts array).
|
|
166
|
+
// Exposed on the barrel so non-SSR renderers — notably the meno-astro dialect
|
|
167
|
+
// BaseLayout — can produce identical font CSS without the server-side config cache.
|
|
168
|
+
export { fontFaceCss, fontPreloadLinks, extractFamilyName } from './fontCss';
|
|
169
|
+
export type { FontConfig } from './fontCss';
|
|
170
|
+
|
|
171
|
+
// Library tag generation (pure: merge/filter/collect + <link>/<script>/<style> tags
|
|
172
|
+
// from a libraries config). Exposed on the barrel so the meno-astro dialect
|
|
173
|
+
// (loadLibraries → BaseLayout) injects external JS/CSS libraries byte-identically to
|
|
174
|
+
// meno-core's SSR (htmlGenerator), without the server-side config cache.
|
|
175
|
+
export {
|
|
176
|
+
mergeLibraries,
|
|
177
|
+
collectComponentLibraries,
|
|
178
|
+
filterLibrariesByContext,
|
|
179
|
+
generateLibraryTags,
|
|
180
|
+
generateScriptTag,
|
|
181
|
+
generateStylesheetTag,
|
|
182
|
+
generateInlineStyleTag,
|
|
183
|
+
} from './libraryLoader';
|
|
184
|
+
export type { LibraryTags } from './libraryLoader';
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { inlineSvgStyleRules } from './inlineSvgStyleRules';
|
|
3
|
+
|
|
4
|
+
describe('inlineSvgStyleRules', () => {
|
|
5
|
+
test('no-op when no <svg>', () => {
|
|
6
|
+
const html = '<div><style>.x { color: red }</style></div>';
|
|
7
|
+
expect(inlineSvgStyleRules(html)).toBe(html);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test('no-op when no <style>', () => {
|
|
11
|
+
const html = '<svg><path d="M0 0"/></svg>';
|
|
12
|
+
expect(inlineSvgStyleRules(html)).toBe(html);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('Illustrator pattern: <defs><style>.cls-1{fill:currentColor;}</style></defs>', () => {
|
|
16
|
+
const input = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><style>.cls-1{fill:currentColor;}</style></defs><path class="cls-1" d="M16 2L2 30h28L16 2z"/></svg>`;
|
|
17
|
+
const out = inlineSvgStyleRules(input);
|
|
18
|
+
expect(out).toContain('style="fill:currentColor"');
|
|
19
|
+
expect(out).toContain('class="cls-1"');
|
|
20
|
+
expect(out).not.toContain('<style>');
|
|
21
|
+
expect(out).not.toContain('<defs>');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('multiple paths sharing the class get the style each', () => {
|
|
25
|
+
const input = `<svg><defs><style>.cls-1{fill:currentColor;}</style></defs><g><path class="cls-1" d="M0 0"/><path class="cls-1" d="M1 1"/></g></svg>`;
|
|
26
|
+
const out = inlineSvgStyleRules(input);
|
|
27
|
+
const count = (out.match(/style="fill:currentColor"/g) || []).length;
|
|
28
|
+
expect(count).toBe(2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('multiple classes', () => {
|
|
32
|
+
const input = `<svg><defs><style>.cls-1{fill:#fff;}.cls-2{fill:#000;}</style></defs><path class="cls-1" d="M0 0"/><path class="cls-2" d="M1 1"/></svg>`;
|
|
33
|
+
const out = inlineSvgStyleRules(input);
|
|
34
|
+
expect(out).toContain('<path class="cls-1" d="M0 0" style="fill:#fff"');
|
|
35
|
+
expect(out).toContain('<path class="cls-2" d="M1 1" style="fill:#000"');
|
|
36
|
+
expect(out).not.toContain('<style>');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('merges with existing style="" attribute on the element', () => {
|
|
40
|
+
const input = `<svg><defs><style>.cls-1{fill:currentColor;}</style></defs><path class="cls-1" style="opacity:0.5" d="M0 0"/></svg>`;
|
|
41
|
+
const out = inlineSvgStyleRules(input);
|
|
42
|
+
expect(out).toMatch(/style="opacity:0\.5;\s*fill:currentColor"/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('class with extra siblings in the class list still matches', () => {
|
|
46
|
+
const input = `<svg><defs><style>.cls-1{fill:red;}</style></defs><path class="other cls-1 more" d="M0 0"/></svg>`;
|
|
47
|
+
const out = inlineSvgStyleRules(input);
|
|
48
|
+
expect(out).toContain('style="fill:red"');
|
|
49
|
+
expect(out).toContain('class="other cls-1 more"');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('multi-declaration rules', () => {
|
|
53
|
+
const input = `<svg><defs><style>.cls-1{fill:red;stroke:blue;stroke-width:2;}</style></defs><path class="cls-1" d="M0 0"/></svg>`;
|
|
54
|
+
const out = inlineSvgStyleRules(input);
|
|
55
|
+
expect(out).toContain('style="fill:red;stroke:blue;stroke-width:2"');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('leaves unknown CSS untouched (media query)', () => {
|
|
59
|
+
const input = `<svg><defs><style>.cls-1{fill:red;}@media (prefers-color-scheme:dark){.cls-1{fill:blue;}}</style></defs><path class="cls-1" d="M0 0"/></svg>`;
|
|
60
|
+
const out = inlineSvgStyleRules(input);
|
|
61
|
+
// We bail rather than partially apply.
|
|
62
|
+
expect(out).toContain('<style>');
|
|
63
|
+
expect(out).toContain('@media');
|
|
64
|
+
expect(out).not.toContain('style="fill:red"');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('leaves complex selectors untouched (descendant)', () => {
|
|
68
|
+
const input = `<svg><defs><style>g .cls-1{fill:red;}</style></defs><g><path class="cls-1" d="M0 0"/></g></svg>`;
|
|
69
|
+
const out = inlineSvgStyleRules(input);
|
|
70
|
+
expect(out).toContain('<style>');
|
|
71
|
+
expect(out).not.toContain('style="fill:red"');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('CSS comments inside <style> are ignored', () => {
|
|
75
|
+
const input = `<svg><defs><style>/* generated by Illustrator */.cls-1{fill:currentColor;}</style></defs><path class="cls-1" d="M0 0"/></svg>`;
|
|
76
|
+
const out = inlineSvgStyleRules(input);
|
|
77
|
+
expect(out).toContain('style="fill:currentColor"');
|
|
78
|
+
expect(out).not.toContain('<style>');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('multiple SVGs in the same string are processed independently', () => {
|
|
82
|
+
const a = `<svg><defs><style>.a{fill:red;}</style></defs><path class="a" d="M0 0"/></svg>`;
|
|
83
|
+
const b = `<svg><defs><style>.b{fill:blue;}</style></defs><path class="b" d="M1 1"/></svg>`;
|
|
84
|
+
const out = inlineSvgStyleRules(a + b);
|
|
85
|
+
expect(out).toContain('<path class="a" d="M0 0" style="fill:red"');
|
|
86
|
+
expect(out).toContain('<path class="b" d="M1 1" style="fill:blue"');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('preserves outer wrappers/groups', () => {
|
|
90
|
+
const input = `<svg id="Layer_2"><defs><style>.cls-1{fill:currentColor;}</style></defs><g id="Layer_1-2"><g><path class="cls-1" d="M0 0"/></g></g></svg>`;
|
|
91
|
+
const out = inlineSvgStyleRules(input);
|
|
92
|
+
expect(out).toContain('<svg id="Layer_2">');
|
|
93
|
+
expect(out).toContain('<g id="Layer_1-2">');
|
|
94
|
+
expect(out).toContain('style="fill:currentColor"');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('self-closing path syntax is supported', () => {
|
|
98
|
+
const input = `<svg><defs><style>.cls-1{fill:red;}</style></defs><path class="cls-1" d="M0 0"/></svg>`;
|
|
99
|
+
const out = inlineSvgStyleRules(input);
|
|
100
|
+
expect(out).toContain('style="fill:red"/>');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('open-close path syntax is supported', () => {
|
|
104
|
+
const input = `<svg><defs><style>.cls-1{fill:red;}</style></defs><path class="cls-1" d="M0 0"></path></svg>`;
|
|
105
|
+
const out = inlineSvgStyleRules(input);
|
|
106
|
+
expect(out).toContain('style="fill:red"></path>');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline simple SVG `<style>` rules as `style=""` attributes on matching
|
|
3
|
+
* elements. Works around a Chromium quirk where SVG `<style>` elements
|
|
4
|
+
* (re-)inserted via `innerHTML` (e.g. by the editor preview's HMR
|
|
5
|
+
* `smartUpdate` fallback at `ssr/htmlGenerator.ts:574`) parse cleanly but
|
|
6
|
+
* never register with the CSSOM, leaving class-scoped rules dead. Inline
|
|
7
|
+
* `style=""` is an attribute and renders regardless of CSSOM state.
|
|
8
|
+
*
|
|
9
|
+
* Only handles the Adobe Illustrator / Figma export pattern: a single class
|
|
10
|
+
* selector with simple `prop: val;` declarations. Anything more exotic
|
|
11
|
+
* (pseudo-classes, media queries, complex selectors, `@`-rules) is left in
|
|
12
|
+
* place untouched.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// `.cls-1 { fill: currentColor; color: red }` — single class selector, body
|
|
16
|
+
// of plain declarations.
|
|
17
|
+
const RULE_RE = /\.([A-Za-z_][\w-]*)\s*\{\s*([^{}]*?)\s*\}/g;
|
|
18
|
+
const COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
|
19
|
+
|
|
20
|
+
function stripCssComments(css: string): string {
|
|
21
|
+
return css.replace(COMMENT_RE, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// True when `body` is the only content of the <style> after we strip out
|
|
25
|
+
// the rules we matched. If anything else is left (an @media, a tag selector,
|
|
26
|
+
// a compound selector), we bail and leave the <style> in place.
|
|
27
|
+
function onlySimpleRules(cssAfterStrip: string): boolean {
|
|
28
|
+
return cssAfterStrip.replace(/\s+/g, '') === '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function mergeIntoStyleAttr(existing: string, additions: string): string {
|
|
32
|
+
const cleanExisting = existing.trim().replace(/;$/, '');
|
|
33
|
+
const cleanAdditions = additions.trim().replace(/;$/, '');
|
|
34
|
+
if (!cleanExisting) return cleanAdditions;
|
|
35
|
+
if (!cleanAdditions) return cleanExisting;
|
|
36
|
+
return `${cleanExisting}; ${cleanAdditions}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function escapeForRegex(s: string): string {
|
|
40
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Process one `<svg>...</svg>` block: find any `<style>` children, inline
|
|
45
|
+
* their simple class rules onto elements within the same SVG, and remove the
|
|
46
|
+
* `<style>` block (and the surrounding empty `<defs>` if that's all it held).
|
|
47
|
+
* Returns the rewritten block, or the original on bail.
|
|
48
|
+
*/
|
|
49
|
+
function rewriteSvgBlock(svgBlock: string): string {
|
|
50
|
+
const styleBlockRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
|
|
51
|
+
|
|
52
|
+
// Per-class merged declarations, in case multiple rules target the same class.
|
|
53
|
+
const classDeclarations = new Map<string, string>();
|
|
54
|
+
const styleBlocksHandled: string[] = [];
|
|
55
|
+
|
|
56
|
+
let m: RegExpExecArray | null;
|
|
57
|
+
while ((m = styleBlockRe.exec(svgBlock)) !== null) {
|
|
58
|
+
const fullStyleTag = m[0];
|
|
59
|
+
const rawContent = m[1];
|
|
60
|
+
// Strip CDATA wrapper if present (DOMPurify already unwraps it, but the
|
|
61
|
+
// pre-sanitize call path may see one).
|
|
62
|
+
const content = stripCssComments(
|
|
63
|
+
rawContent.replace(/^\s*<!\[CDATA\[/, '').replace(/\]\]>\s*$/, '')
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Pull out every simple `.cls { ... }` rule we can match.
|
|
67
|
+
const localClasses = new Map<string, string>();
|
|
68
|
+
let remaining = content;
|
|
69
|
+
let ruleMatch: RegExpExecArray | null;
|
|
70
|
+
const ruleRe = new RegExp(RULE_RE.source, 'g');
|
|
71
|
+
while ((ruleMatch = ruleRe.exec(content)) !== null) {
|
|
72
|
+
const className = ruleMatch[1];
|
|
73
|
+
const body = ruleMatch[2].trim().replace(/;\s*$/, '');
|
|
74
|
+
// Skip rules whose body contains a `{` — that means we mis-bracketed.
|
|
75
|
+
if (body.includes('{')) continue;
|
|
76
|
+
const prev = localClasses.get(className);
|
|
77
|
+
localClasses.set(className, prev ? mergeIntoStyleAttr(prev, body) : body);
|
|
78
|
+
remaining = remaining.replace(ruleMatch[0], '');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// If anything non-trivial is left in the <style> body, leave the block
|
|
82
|
+
// alone — we don't want to silently drop @media, tag selectors, etc.
|
|
83
|
+
if (!onlySimpleRules(remaining)) continue;
|
|
84
|
+
if (localClasses.size === 0) continue;
|
|
85
|
+
|
|
86
|
+
for (const [cls, decls] of localClasses) {
|
|
87
|
+
const prev = classDeclarations.get(cls);
|
|
88
|
+
classDeclarations.set(cls, prev ? mergeIntoStyleAttr(prev, decls) : decls);
|
|
89
|
+
}
|
|
90
|
+
styleBlocksHandled.push(fullStyleTag);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (classDeclarations.size === 0) return svgBlock;
|
|
94
|
+
|
|
95
|
+
let out = svgBlock;
|
|
96
|
+
|
|
97
|
+
// Inline each class's declarations onto every matching element inside this
|
|
98
|
+
// SVG. Match opening tags (self-closing or not) that contain `class="…cls…"`.
|
|
99
|
+
for (const [className, decls] of classDeclarations) {
|
|
100
|
+
const elementRe = new RegExp(
|
|
101
|
+
`(<[A-Za-z][\\w-]*\\b[^>]*?\\sclass="[^"]*\\b${escapeForRegex(className)}\\b[^"]*"[^>]*?)(/?>)`,
|
|
102
|
+
'g'
|
|
103
|
+
);
|
|
104
|
+
out = out.replace(elementRe, (_full, openHead: string, closing: string) => {
|
|
105
|
+
const styleAttrMatch = openHead.match(/\sstyle="([^"]*)"/);
|
|
106
|
+
if (styleAttrMatch) {
|
|
107
|
+
const merged = mergeIntoStyleAttr(styleAttrMatch[1], decls);
|
|
108
|
+
return openHead.replace(/\sstyle="[^"]*"/, ` style="${merged}"`) + closing;
|
|
109
|
+
}
|
|
110
|
+
return `${openHead} style="${decls}"${closing}`;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Drop the <style> blocks we consumed.
|
|
115
|
+
for (const block of styleBlocksHandled) {
|
|
116
|
+
out = out.replace(block, '');
|
|
117
|
+
}
|
|
118
|
+
// Tidy: collapse <defs> that we just emptied.
|
|
119
|
+
out = out.replace(/<defs>\s*<\/defs>/g, '');
|
|
120
|
+
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Pre-process embed HTML so SVG `<style>` rules survive an `innerHTML`
|
|
126
|
+
* reparse in the editor preview iframe. Safe to call on any string — does
|
|
127
|
+
* nothing if no `<svg>` or no `<style>` is present.
|
|
128
|
+
*/
|
|
129
|
+
export function inlineSvgStyleRules(html: string): string {
|
|
130
|
+
if (!html) return html;
|
|
131
|
+
if (html.indexOf('<svg') === -1 || html.indexOf('<style') === -1) return html;
|
|
132
|
+
|
|
133
|
+
return html.replace(/<svg\b[^>]*>[\s\S]*?<\/svg>/gi, (svgBlock) => rewriteSvgBlock(svgBlock));
|
|
134
|
+
}
|