@symbo.ls/mermaid 3.14.1 → 3.14.4
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/package.json +2 -2
- package/src/bundle.js +65 -17
- package/src/render.js +81 -5
- package/src/template.html +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@symbo.ls/mermaid",
|
|
3
|
-
"version": "3.14.
|
|
3
|
+
"version": "3.14.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "CC-BY-NC-4.0",
|
|
6
6
|
"description": "Self-sustaining server runner for Symbols projects from JSON. Renders DOMQL projects to HTML with optional brender SSR.",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"mermaid": "./bin/start.js"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@symbo.ls/utils": "^3.14.
|
|
32
|
+
"@symbo.ls/utils": "^3.14.2",
|
|
33
33
|
"cors": "^2.8.5",
|
|
34
34
|
"express": "^4.21.2"
|
|
35
35
|
},
|
package/src/bundle.js
CHANGED
|
@@ -6,6 +6,15 @@ import smblsIife from 'smbls/iife-string';
|
|
|
6
6
|
|
|
7
7
|
log.debug(`[BUNDLE] smbls IIFE loaded from smbls/iife-string (${smblsIife.length} bytes)`);
|
|
8
8
|
|
|
9
|
+
// FRAMEWORK-6: any `<` in JSON that gets inlined into a `<script>` block can
|
|
10
|
+
// terminate the wrapper (`</script>`) or trigger HTML-comment / CDATA modes
|
|
11
|
+
// (`<!--`, `<![CDATA[`) inside the script-data state. Replacing every `<`
|
|
12
|
+
// with the JS unicode escape `<` neutralizes HTML interpretation while
|
|
13
|
+
// preserving the runtime string value — `<` decodes to `<` at JS parse
|
|
14
|
+
// time, so the embedded object is identical to the original. Standard
|
|
15
|
+
// mitigation: see OWASP §RULE-3.1, React's renderToString, Rails json_escape.
|
|
16
|
+
const escapeJsonForScript = (s) => s.replace(/</g, '\\u003c');
|
|
17
|
+
|
|
9
18
|
export function getSmblsIife() {
|
|
10
19
|
return smblsIife;
|
|
11
20
|
}
|
|
@@ -45,8 +54,10 @@ export function getBundleScript(appKey, data, req, { ProjectDataService } = {})
|
|
|
45
54
|
log.error(`[BUNDLE] Failed to ensure merged data: ${mergeError.message}`);
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
// Serialize the project data as JSON (same as templates __mockedData approach)
|
|
49
|
-
|
|
57
|
+
// Serialize the project data as JSON (same as templates __mockedData approach).
|
|
58
|
+
// escapeJsonForScript: see FRAMEWORK-6 — any `</script>` (or `<!--`) inside a
|
|
59
|
+
// string value would break out of the inline `<script>` wrapper.
|
|
60
|
+
const jsonData = escapeJsonForScript(JSON.stringify(data));
|
|
50
61
|
|
|
51
62
|
/*
|
|
52
63
|
* Determine liveSync setting.
|
|
@@ -93,13 +104,13 @@ export function getBundleScript(appKey, data, req, { ProjectDataService } = {})
|
|
|
93
104
|
(data && (data._id || data.projectId || data.id)) ||
|
|
94
105
|
null;
|
|
95
106
|
const settingsStr = isSymboLs
|
|
96
|
-
? JSON.stringify({
|
|
107
|
+
? escapeJsonForScript(JSON.stringify({
|
|
97
108
|
...data.settings,
|
|
98
109
|
liveSync: !shouldDisableLiveSync,
|
|
99
110
|
inspect: true,
|
|
100
111
|
verbose: !shouldDisableLiveSync,
|
|
101
112
|
projectId,
|
|
102
|
-
})
|
|
113
|
+
}))
|
|
103
114
|
: 'null';
|
|
104
115
|
|
|
105
116
|
// Generate the client script - same pattern as templates/start.js runFromJson
|
|
@@ -124,23 +135,55 @@ export function getBundleScript(appKey, data, req, { ProjectDataService } = {})
|
|
|
124
135
|
var settings = ${settingsStr};
|
|
125
136
|
if (settings) context.editor = settings;
|
|
126
137
|
|
|
127
|
-
// When brender pre-rendered this page, adopt the existing DOM nodes
|
|
128
|
-
// instead of tearing them down. The [data-ssr] wrapper is moved out of the
|
|
129
|
-
// way so pre-rendered children land at document.body level — matching the
|
|
130
|
-
// level at which DOMQL creates its own elements. The __BRENDER__ flag and
|
|
131
|
-
// __BR_REGISTRY__ map were injected into <head> by the server.
|
|
132
138
|
// Theme initialization: set data-theme before app renders to avoid FOUC.
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
+
// Mirrors the precedence in resolveAndApplyTheme (packages/smbls/src/index.js):
|
|
140
|
+
// 1. URL param ?globalTheme=<name> — embedded-iframe override
|
|
141
|
+
// 2. localStorage[themeStorageKey] — persisted user choice
|
|
142
|
+
// 3. context.globalTheme — project's configured value
|
|
143
|
+
// 4. matchMedia(prefers-color-scheme: dark) — OS fallback (when 'auto')
|
|
144
|
+
//
|
|
145
|
+
// Writing the literal 'auto' as data-theme would leave the
|
|
146
|
+
// [data-theme="dark"] / [data-theme="light"] CSS selectors unmatched and
|
|
147
|
+
// the page would render with system defaults instead of the chosen
|
|
148
|
+
// theme — so 'auto' is always resolved to a concrete light/dark value.
|
|
149
|
+
//
|
|
150
|
+
// PRIOR BUG: this script previously ignored context.globalTheme entirely
|
|
151
|
+
// and went straight to matchMedia after localStorage. A project with
|
|
152
|
+
// globalTheme: light in config.js would render in OS dark on a
|
|
153
|
+
// dark-mode device because the inline boot ran first and locked in
|
|
154
|
+
// dark before resolveAndApplyTheme could correct it.
|
|
155
|
+
//
|
|
156
|
+
// IDEMPOTENT GUARD: the SSR-side render in @symbo.ls/mermaid/render.js
|
|
157
|
+
// now bakes data-theme onto <html> for concrete-theme projects and
|
|
158
|
+
// emits an inline <head> script that runs synchronously before paint.
|
|
159
|
+
// When that path runs, data-theme is already set by the time the bundle
|
|
160
|
+
// executes — skip this block to avoid re-resolving and overwriting
|
|
161
|
+
// (which would race with URL/localStorage values the head script
|
|
162
|
+
// already honored). Kept as a safety net for older renders / cases
|
|
163
|
+
// where the head script couldn't run.
|
|
164
|
+
if (
|
|
165
|
+
context.useDocumentTheme !== false &&
|
|
166
|
+
!document.documentElement.hasAttribute('data-theme')
|
|
167
|
+
) {
|
|
139
168
|
(function() {
|
|
140
169
|
try {
|
|
141
170
|
var t = null;
|
|
142
|
-
|
|
143
|
-
|
|
171
|
+
// 1. URL param wins (used by preview/canvas iframe embedding).
|
|
172
|
+
try {
|
|
173
|
+
var p = new URLSearchParams(window.location.search).get('globalTheme');
|
|
174
|
+
if (p) t = p;
|
|
175
|
+
} catch(e) {}
|
|
176
|
+
// 2. localStorage — only consulted when no forcing source above.
|
|
177
|
+
if (!t) {
|
|
178
|
+
var key = context.themeStorageKey;
|
|
179
|
+
if (key) { try { t = localStorage.getItem(key) } catch(e) {} }
|
|
180
|
+
}
|
|
181
|
+
// 3. Project's configured globalTheme (config.js export). Honors an
|
|
182
|
+
// explicit 'light'/'dark'/<scheme> instead of falling through to OS.
|
|
183
|
+
if (!t && context.globalTheme && context.globalTheme !== 'auto') {
|
|
184
|
+
t = context.globalTheme;
|
|
185
|
+
}
|
|
186
|
+
// 4. OS preference fallback — only when nothing concrete chose.
|
|
144
187
|
if (!t || t === 'auto') {
|
|
145
188
|
t = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
146
189
|
}
|
|
@@ -149,6 +192,11 @@ export function getBundleScript(appKey, data, req, { ProjectDataService } = {})
|
|
|
149
192
|
})();
|
|
150
193
|
}
|
|
151
194
|
|
|
195
|
+
// When brender pre-rendered this page, adopt the existing DOM nodes
|
|
196
|
+
// instead of tearing them down. The [data-ssr] wrapper is moved out of
|
|
197
|
+
// the way so pre-rendered children land at document.body level — matching
|
|
198
|
+
// the level at which DOMQL creates its own elements. The __BRENDER__ flag
|
|
199
|
+
// and __BR_REGISTRY__ map were injected into <head> by the server.
|
|
152
200
|
var ssrShell = document.querySelector('[data-ssr]');
|
|
153
201
|
if (window.__BRENDER__ && ssrShell) {
|
|
154
202
|
// Move pre-rendered children to body before DOMQL skeleton creation
|
package/src/render.js
CHANGED
|
@@ -37,12 +37,22 @@ export { generateSitemap, prefetchPageData };
|
|
|
37
37
|
// ── Default HTML template ───────────────────────────────────────────────────
|
|
38
38
|
// Inlined so the plugin works in any runtime (Node, CF Workers, browser).
|
|
39
39
|
// Mirrors src/template.html — keep in sync.
|
|
40
|
+
//
|
|
41
|
+
// ROOT_ATTRS + HEAD_BOOT eliminate the FOUC that used to happen between
|
|
42
|
+
// HTML-parse-finish and the end-of-body bundle IIFE running (~1s on a
|
|
43
|
+
// 2.5 MB bundle). For projects with a concrete `globalTheme` (e.g. 'light'),
|
|
44
|
+
// SSR bakes `<html data-theme="light">` so first paint is on the right
|
|
45
|
+
// theme. For 'auto' projects, the inline HEAD_BOOT script resolves URL
|
|
46
|
+
// override / localStorage / matchMedia synchronously before any CSS
|
|
47
|
+
// applies. The end-of-body IIFE in bundle.js is kept as an idempotent
|
|
48
|
+
// safety net.
|
|
40
49
|
export const DEFAULT_TEMPLATE_HTML = `<!doctype html>
|
|
41
|
-
<html>
|
|
50
|
+
<html'\${{{ ROOT_ATTRS }}}'>
|
|
42
51
|
<head>
|
|
52
|
+
<meta charset="UTF-8" />
|
|
53
|
+
'\${{{ HEAD_BOOT }}}'
|
|
43
54
|
'\${{{ META }}}'
|
|
44
55
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
45
|
-
<meta charset="UTF-8" />
|
|
46
56
|
'\${{{ IMPORTMAP }}}'
|
|
47
57
|
</head>
|
|
48
58
|
<body>
|
|
@@ -113,7 +123,12 @@ export function transpileFunctionStrings(obj, target = Array.isArray(obj) ? [] :
|
|
|
113
123
|
* @param {object} options
|
|
114
124
|
* @param {string} options.appkey — appkey identifier (for metadata, comments)
|
|
115
125
|
* @param {string} options.pathname — request pathname
|
|
116
|
-
* @param {string} [options.channel='production'] —
|
|
126
|
+
* @param {string} [options.channel='production'] — sysEnv label (production|upcoming|next|...);
|
|
127
|
+
* informational, used for metadata. NOT the same axis as userEnv — sysEnv is which
|
|
128
|
+
* deployment served the request, userEnv is which version of project content.
|
|
129
|
+
* @param {string} [options.userEnv='production'] — projectEnv (production|staging|development|...);
|
|
130
|
+
* drives the SSR-vs-CSR decision and the `<!-- symbols | … -->` comment. Defaults
|
|
131
|
+
* to channel for back-compat with callers that haven't been updated.
|
|
117
132
|
* @param {boolean} [options.isProd=false] — controls SEO indexing (custom domains only)
|
|
118
133
|
* @param {object} [options.req] — full request object (Express-like) for getBundleScript context
|
|
119
134
|
* @param {object} [options.query] — query params (when no req available)
|
|
@@ -131,6 +146,7 @@ export async function renderProjectHtml(data, options = {}) {
|
|
|
131
146
|
appkey,
|
|
132
147
|
pathname = '/',
|
|
133
148
|
channel = 'production',
|
|
149
|
+
userEnv = channel,
|
|
134
150
|
isProd = false,
|
|
135
151
|
req,
|
|
136
152
|
query,
|
|
@@ -156,7 +172,12 @@ export async function renderProjectHtml(data, options = {}) {
|
|
|
156
172
|
|
|
157
173
|
const route = normalizeRoute(pathname);
|
|
158
174
|
const resolved = resolvePageRoute(data.pages, pathname);
|
|
159
|
-
|
|
175
|
+
// SSR is a projectEnv decision — render published (production) and pre-published
|
|
176
|
+
// (staging) slots server-side; render dev slot client-side. Was previously
|
|
177
|
+
// checking `channel` (sysEnv), which gave wrong answers on canonical hosts
|
|
178
|
+
// like *.upcoming.symbo.ls (sysEnv=upcoming, userEnv=production → was CSR
|
|
179
|
+
// when it should have been SSR).
|
|
180
|
+
const shouldBrender = userEnv === 'production' || userEnv === 'staging';
|
|
160
181
|
|
|
161
182
|
// ── Brender / SSR ─────────────────────────────────────────────────────────
|
|
162
183
|
let prerenderedBody = '';
|
|
@@ -359,13 +380,68 @@ export async function renderProjectHtml(data, options = {}) {
|
|
|
359
380
|
? `<script defer type="text/javascript">${editorBundle}</script>`
|
|
360
381
|
: '';
|
|
361
382
|
|
|
383
|
+
// Comment label is the projectEnv (which version of the content is being
|
|
384
|
+
// served). Was previously `${channel}` (sysEnv) which mislabelled canonical
|
|
385
|
+
// *.upcoming.symbo.ls hosts as "upcoming" when the actual projectEnv is
|
|
386
|
+
// "production".
|
|
362
387
|
const envComment = isProd
|
|
363
388
|
? `<!-- built with symbols.app -->\n`
|
|
364
|
-
: `<!-- symbols | ${appkey} | ${
|
|
389
|
+
: `<!-- symbols | ${appkey} | ${userEnv} | ${shouldBrender ? 'ssr' : 'csr'} | noindex -->\n`;
|
|
390
|
+
|
|
391
|
+
// ── Theme: zero-flash boot ─────────────────────────────────────────────────
|
|
392
|
+
// Eliminates the dark→light (or light→dark) flash that used to happen
|
|
393
|
+
// between first paint and the end-of-body bundle IIFE setting data-theme.
|
|
394
|
+
// Two layers:
|
|
395
|
+
// 1. Concrete `globalTheme` ('light'|'dark'|<custom>) → bake `data-theme`
|
|
396
|
+
// onto <html> server-side. Zero JS, perfect first paint.
|
|
397
|
+
// 2. URL ?globalTheme= override / localStorage user choice / 'auto' OS
|
|
398
|
+
// pref → small inline <head> script resolves synchronously before
|
|
399
|
+
// any CSS applies. Browser renders first paint with the resolved
|
|
400
|
+
// data-theme, no flash.
|
|
401
|
+
// Layer 3 (existing end-of-body IIFE in bundle.js) is the back-compat
|
|
402
|
+
// safety net for cases the head script didn't reach.
|
|
403
|
+
const useDocumentTheme = data.useDocumentTheme !== false;
|
|
404
|
+
const concreteTheme =
|
|
405
|
+
data.globalTheme && data.globalTheme !== 'auto' ? String(data.globalTheme) : null;
|
|
406
|
+
const themeStorageKey = data.themeStorageKey ? String(data.themeStorageKey) : '';
|
|
407
|
+
|
|
408
|
+
// SSR-baked attribute on <html>. Only when the project has a concrete
|
|
409
|
+
// theme — for 'auto' we leave <html> bare so the head script (or
|
|
410
|
+
// CSS @media) can decide. Custom schemes (e.g. 'ocean') flow through.
|
|
411
|
+
const rootAttrs =
|
|
412
|
+
useDocumentTheme && concreteTheme ? ' data-theme="' + concreteTheme + '"' : '';
|
|
413
|
+
|
|
414
|
+
// Inline head boot. Runs synchronously during HTML parse, before any
|
|
415
|
+
// CSS applies. Order matches resolveAndApplyTheme in
|
|
416
|
+
// smbls/packages/smbls/src/index.js:
|
|
417
|
+
// 1. ?globalTheme= URL param (preview/editor iframes push themes here)
|
|
418
|
+
// 2. localStorage[themeStorageKey] (persisted user toggle)
|
|
419
|
+
// 3. concreteTheme baked from project config (for 'auto' projects this
|
|
420
|
+
// is null and we fall to step 4)
|
|
421
|
+
// 4. matchMedia(prefers-color-scheme: dark) — OS fallback
|
|
422
|
+
// Skipped entirely when useDocumentTheme is false.
|
|
423
|
+
const headBoot = useDocumentTheme
|
|
424
|
+
? '<script>(function(){try{var t=null;' +
|
|
425
|
+
"try{t=new URLSearchParams(location.search).get('globalTheme')}catch(e){}" +
|
|
426
|
+
(themeStorageKey
|
|
427
|
+
? 'if(!t){try{t=localStorage.getItem(' +
|
|
428
|
+
JSON.stringify(themeStorageKey) +
|
|
429
|
+
')}catch(e){}}'
|
|
430
|
+
: '') +
|
|
431
|
+
"if(!t||t==='auto'){" +
|
|
432
|
+
(concreteTheme
|
|
433
|
+
? 't=' + JSON.stringify(concreteTheme) + ';'
|
|
434
|
+
: "try{t=window.matchMedia&&window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'}catch(e){}") +
|
|
435
|
+
'}' +
|
|
436
|
+
"if(t&&t!=='auto')document.documentElement.setAttribute('data-theme',t);" +
|
|
437
|
+
'}catch(e){}})();</script>\n <meta name="color-scheme" content="light dark">'
|
|
438
|
+
: '';
|
|
365
439
|
|
|
366
440
|
const html = replaceVariablesInFileSync(
|
|
367
441
|
envComment + indexHtml,
|
|
368
442
|
{
|
|
443
|
+
ROOT_ATTRS: rootAttrs,
|
|
444
|
+
HEAD_BOOT: headBoot,
|
|
369
445
|
META: metaTags + '\n' + brenderStyles + '\n' + hydrationScript + '\n' + authScript,
|
|
370
446
|
BUNDLE: bundle,
|
|
371
447
|
BODY: prerenderedBody,
|
package/src/template.html
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html>
|
|
2
|
+
<html'${{{ ROOT_ATTRS }}}'>
|
|
3
3
|
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
'${{{ HEAD_BOOT }}}'
|
|
4
6
|
'${{{ META }}}'
|
|
5
7
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
-
<meta charset="UTF-8" />
|
|
7
8
|
'${{{ IMPORTMAP }}}'
|
|
8
9
|
</head>
|
|
9
10
|
<body>
|