@cocreate/render 1.53.0 → 2.0.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.
@@ -0,0 +1,778 @@
1
+ import { checkValue, ObjectId, HTMLScanner, getValueAttribute, transformValue, convertValue } from "@cocreate/utils";
2
+
3
+
4
+ function transformChunk(element, value) {
5
+ value = transformValue(element, value, "set-value");
6
+ value = convertValue(value, getValueAttribute(element, "type", "set-value") || "");
7
+ return typeof value === "object" ? JSON.stringify(value, null, 2) : String(value);
8
+ }
9
+
10
+ // Elements that should not have generic [src] fetch logic applied
11
+ const SKIP_SRC_ELEMENTS = new Set([
12
+ "SCRIPT", "IMG", "IFRAME", "AUDIO", "VIDEO", "SOURCE", "TRACK", "INPUT", "EMBED", "FRAME"
13
+ ]);
14
+
15
+ // Element types ignored during data attribute rendering
16
+ const IGNORE_ELEMENTS = new Set([
17
+ "FORM", "INPUT", "TEXTAREA", "SELECT", "LINK", "IFRAME", "COCREATE-SELECT"
18
+ ]);
19
+
20
+ // Default maximum allowed recursion depth (0 disables the depth limit for infinite scaling)
21
+ const DEFAULT_MAX_DEPTH = 25;
22
+
23
+ function parseState(value) {
24
+ if (!value) return {};
25
+ if (typeof value === "object" && !Array.isArray(value)) return value;
26
+ if (typeof value !== "string") return {};
27
+
28
+ try {
29
+ const parsed = JSON.parse(value);
30
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
31
+ ? parsed
32
+ : {};
33
+ } catch (_) {
34
+ return {};
35
+ }
36
+ }
37
+
38
+ function isEnabled(value) {
39
+ return value === "" || (Boolean(value) && value !== "false");
40
+ }
41
+
42
+ function applyState(doc, state) {
43
+ if (!state || !Object.keys(state).length) return;
44
+
45
+ const elements = doc.querySelectorAll({ attributes: ["state-id"] });
46
+ for (const element of elements) {
47
+ const stateIds = String(element.getAttribute("state-id") || "")
48
+ .split(",")
49
+ .map(value => value.trim())
50
+ .filter(Boolean);
51
+
52
+ for (const stateId of stateIds) {
53
+ const values = state[stateId];
54
+ if (!values || typeof values !== "object" || Array.isArray(values)) continue;
55
+
56
+ const overwriteAttribute = element.getAttribute("state-overwrite");
57
+ const overwrite = isEnabled(
58
+ overwriteAttribute === null || overwriteAttribute === undefined
59
+ ? values.overwrite
60
+ : overwriteAttribute
61
+ );
62
+
63
+ for (const [attribute, value] of Object.entries(values)) {
64
+ if (value === undefined || value === null || attribute.startsWith("$")) continue;
65
+
66
+ if (attribute === "class" && value) {
67
+ const classes = new Set(
68
+ String(element.getAttribute("class") || "")
69
+ .split(/\s+/)
70
+ .filter(Boolean)
71
+ );
72
+ for (const name of String(value).split(/\s+/).filter(Boolean)) {
73
+ classes.add(name);
74
+ }
75
+ element.setAttribute("class", [...classes].join(" "));
76
+ continue;
77
+ }
78
+
79
+ const currentValue = element.getAttribute(attribute);
80
+ if (
81
+ element.hasAttribute(attribute) &&
82
+ (!currentValue || overwrite)
83
+ ) {
84
+ element.setAttribute(attribute, String(value));
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Extracts <head> content from chunk HTML and merges title, meta, link, script tags into root HTML head.
93
+ * Completely powered by HTMLScanner handles.
94
+ * @param {string} rootHtml - Main host HTML string
95
+ * @param {string} chunkHtml - Fetched chunk HTML string
96
+ * @returns {{ rootHtml: string, cleanChunkHtml: string }}
97
+ * @private
98
+ * @environment server
99
+ */
100
+ function extractAndMergeHead(rootHtml, chunkHtml) {
101
+ const chunkDoc = new HTMLScanner(chunkHtml);
102
+ const headHandles = chunkDoc.querySelectorAll({ tags: ["HEAD"] });
103
+
104
+ if (headHandles.length === 0) {
105
+ return { rootHtml, cleanChunkHtml: chunkHtml };
106
+ }
107
+
108
+ const chunkHeadHandle = headHandles[0];
109
+ const chunkHeadContent = chunkHeadHandle.getInnerHTML();
110
+
111
+ // Remove <head> tag from chunk
112
+ chunkHeadHandle.replaceWith("");
113
+ const cleanChunkHtml = chunkDoc.toString();
114
+
115
+ const rootDoc = new HTMLScanner(rootHtml);
116
+ const rootHeadHandles = rootDoc.querySelectorAll({ tags: ["HEAD"] });
117
+
118
+ if (rootHeadHandles.length > 0) {
119
+ const rootHeadHandle = rootHeadHandles[0];
120
+ const existingContent = rootHeadHandle.getInnerHTML();
121
+ rootHeadHandle.setInnerHTML(`${existingContent}\n${chunkHeadContent}\n`);
122
+ rootHtml = rootDoc.toString();
123
+ } else {
124
+ // Inject <head> into root html tag if missing
125
+ const rootHtmlHandles = rootDoc.querySelectorAll({ tags: ["HTML"] });
126
+ if (rootHtmlHandles.length > 0) {
127
+ const htmlHandle = rootHtmlHandles[0];
128
+ const existingInner = htmlHandle.getInnerHTML();
129
+ htmlHandle.setInnerHTML(`<head>\n${chunkHeadContent}\n</head>\n${existingInner}`);
130
+ rootHtml = rootDoc.toString();
131
+ } else {
132
+ rootHtml = `<head>\n${chunkHeadContent}\n</head>\n` + rootHtml;
133
+ }
134
+ }
135
+
136
+ return { rootHtml, cleanChunkHtml };
137
+ }
138
+
139
+ function extractStylesheetLinks(html, rootHtml = html) {
140
+ // Fragments keep their links in place unless the host already has a head.
141
+ const rootDoc = new HTMLScanner(rootHtml);
142
+ if (!rootDoc.querySelectorAll({ tags: ["HEAD"] }).length) {
143
+ return { cleanHtml: html, links: [] };
144
+ }
145
+ const doc = new HTMLScanner(html);
146
+ const links = [];
147
+ const linkHandles = doc.querySelectorAll({ tags: ["LINK"] });
148
+
149
+ for (const link of linkHandles) {
150
+ const rel = String(link.getAttribute("rel") || "")
151
+ .toLowerCase()
152
+ .split(/\s+/)
153
+ .filter(Boolean);
154
+ if (
155
+ !rel.includes("stylesheet") ||
156
+ !link.getAttribute("href") ||
157
+ link.hasAttribute("array") ||
158
+ link.hasAttribute("object")
159
+ ) continue;
160
+
161
+ links.push({
162
+ html: link.getOuterHTML(),
163
+ href: link.getAttribute("href"),
164
+ media: link.getAttribute("media") || ""
165
+ });
166
+ link.replaceWith("");
167
+ }
168
+
169
+ return { cleanHtml: doc.toString(), links };
170
+ }
171
+
172
+ function mergeStylesheetLinks(html, links) {
173
+ if (!links.length) return html;
174
+
175
+ const unique = new Map();
176
+ for (const link of links) {
177
+ const key = `${link.href}\u0000${link.media}`;
178
+ if (!unique.has(key)) unique.set(key, link.html);
179
+ }
180
+
181
+ const content = [...unique.values()].join("\n");
182
+ const doc = new HTMLScanner(html);
183
+ const headHandles = doc.querySelectorAll({ tags: ["HEAD"] });
184
+
185
+ if (headHandles.length > 0) {
186
+ const head = headHandles[0];
187
+ head.setInnerHTML(`${head.getInnerHTML()}\n${content}\n`);
188
+ return doc.toString();
189
+ }
190
+
191
+ const htmlHandles = doc.querySelectorAll({ tags: ["HTML"] });
192
+ if (htmlHandles.length > 0) {
193
+ const root = htmlHandles[0];
194
+ root.setInnerHTML(`<head>\n${content}\n</head>\n${root.getInnerHTML()}`);
195
+ return doc.toString();
196
+ }
197
+
198
+ return `<head>\n${content}\n</head>\n${html}`;
199
+ }
200
+
201
+ /**
202
+ * Applies theme attribute to <html> tag and injects color-scheme meta tag into <head> using HTMLScanner
203
+ * @param {string} html - Full document HTML string
204
+ * @param {string} theme - Selected theme name (e.g., 'dark')
205
+ * @returns {string} Modified HTML
206
+ * @private
207
+ * @environment server
208
+ */
209
+ function applyTheme(html, theme) {
210
+ const doc = new HTMLScanner(html);
211
+
212
+ // Apply color-scheme attribute on <html> element
213
+ const htmlHandles = doc.querySelectorAll({ tags: ["HTML"] });
214
+ const selectedTheme = htmlHandles[0]?.getAttribute("color-scheme");
215
+ if (selectedTheme === "dark" || selectedTheme === "light") theme = selectedTheme;
216
+ if (theme !== "dark" && theme !== "light") return html;
217
+ if (htmlHandles.length > 0) {
218
+ htmlHandles[0].setAttribute("color-scheme", theme);
219
+ }
220
+
221
+ // Update or inject <meta name="color-scheme">
222
+ const metaHandles = doc.querySelectorAll({ tags: ["META"] });
223
+ let colorSchemeMeta = metaHandles.find(h => h.getAttribute("name") === "color-scheme");
224
+
225
+ if (colorSchemeMeta) {
226
+ colorSchemeMeta.setAttribute("content", theme);
227
+ } else {
228
+ const headHandles = doc.querySelectorAll({ tags: ["HEAD"] });
229
+ if (headHandles.length > 0) {
230
+ const existingHeadContent = headHandles[0].getInnerHTML();
231
+ headHandles[0].setInnerHTML(`${existingHeadContent}\n<meta name="color-scheme" content="${theme}">`);
232
+ }
233
+ }
234
+
235
+ return doc.toString();
236
+ }
237
+
238
+ /**
239
+ * Generates <link rel="alternate" ...> tags for organization languages.
240
+ * @param {Object} file - File object
241
+ * @param {Object} organization - Organization metadata
242
+ * @param {Object} urlObject - Request URL
243
+ * @returns {string} Link tags string
244
+ * @private
245
+ * @environment server
246
+ */
247
+ function createLanguageLinkTags(file, organization, urlObject) {
248
+ if (!organization?.languages?.length) return "";
249
+
250
+ let tags = `<link rel="alternate" hreflang="x-default" href="https://${urlObject.hostname}${file.pathname}">\n`;
251
+ for (const language of organization.languages) {
252
+ const hrefUrl = `https://${urlObject.hostname}/${language}${file.pathname}`;
253
+ tags += `<link rel="alternate" hreflang="${language}" href="${hrefUrl}">\n`;
254
+ }
255
+ return tags;
256
+ }
257
+
258
+ /**
259
+ * Merges target language dictionary entries from a chunk's translation object into the unified translations accumulator.
260
+ * @param {Record<string, Record<string, Object>>} targetMap - Dynamic accumulator map holding merged translations.
261
+ * @param {Record<string, Record<string, Object>>} sourceTranslations - Translation object from a fetched file.
262
+ * @param {string} [langRegion] - Target locale code (e.g., "en-US").
263
+ * @param {string} [lang] - Fallback primary language code (e.g., "en").
264
+ * @private
265
+ * @environment server
266
+ */
267
+ function mergeTranslationSource(targetMap, sourceTranslations, langRegion, lang) {
268
+ if (!sourceTranslations) return;
269
+
270
+ const targetLangKey = (langRegion && sourceTranslations[langRegion])
271
+ ? langRegion
272
+ : (lang && sourceTranslations[lang])
273
+ ? lang
274
+ : null;
275
+
276
+ if (!targetLangKey || !sourceTranslations[targetLangKey]) return;
277
+
278
+ if (!targetMap[targetLangKey]) {
279
+ targetMap[targetLangKey] = {};
280
+ }
281
+
282
+ Object.assign(targetMap[targetLangKey], sourceTranslations[targetLangKey]);
283
+ }
284
+
285
+ /**
286
+ * Fast Language-First Translator operating directly via HTMLScanner handles.
287
+ * Scans for `[i18n]` attributes and replaces element content and attributes.
288
+ * @private
289
+ * @environment server
290
+ */
291
+ function translate(html, translations, langRegion, lang) {
292
+ if (!html || !translations || (!langRegion && !lang)) return html;
293
+
294
+ const langDict = translations[langRegion] || translations[lang];
295
+ if (!langDict) return html;
296
+
297
+ const doc = new HTMLScanner(html);
298
+ const elements = doc.querySelectorAll({ attributes: ["i18n"] });
299
+
300
+ for (const el of elements) {
301
+ const key = el.getAttribute("i18n");
302
+ const values = langDict[key];
303
+ if (!values) continue;
304
+
305
+ for (const [attr, value] of Object.entries(values)) {
306
+ if (attr === "text" || value === undefined || value === null) continue;
307
+ el.setAttribute(attr, String(value));
308
+ }
309
+
310
+ if (values.text !== undefined && values.text !== null && !el.isSelfClosing) {
311
+ let contentStr = "";
312
+ if (el.tagName === "SCRIPT" && typeof values.text === "object") {
313
+ contentStr = JSON.stringify(values.text, null, 2);
314
+ } else {
315
+ contentStr = String(values.text);
316
+ }
317
+ el.setInnerHTML(contentStr);
318
+ }
319
+ }
320
+
321
+ let translatedHtml = doc.toString();
322
+
323
+ // Fallback for application/ld+json script if keyed as "ld+json" without explicit attribute
324
+ if (langDict["ld+json"]) {
325
+ const jsonDoc = new HTMLScanner(translatedHtml);
326
+ const scriptHandles = jsonDoc.querySelectorAll({ tags: ["SCRIPT"] });
327
+ for (const el of scriptHandles) {
328
+ if (el.getAttribute("type") === "application/ld+json") {
329
+ const values = langDict["ld+json"];
330
+ const content = values.text || values;
331
+ const scriptContent = typeof content === "object" ? JSON.stringify(content, null, 2) : String(content);
332
+ el.setInnerHTML(scriptContent);
333
+ break;
334
+ }
335
+ }
336
+ translatedHtml = jsonDoc.toString();
337
+ }
338
+
339
+ return translatedHtml;
340
+ }
341
+
342
+ /**
343
+ * Main functional entry point for Server-Side Rendering using HTMLScanner.
344
+ * @param {Object} options - SSR options
345
+ * @returns {Promise<string>} Fully rendered and translated HTML string
346
+ * @public
347
+ * @environment server
348
+ */
349
+ export async function HTML({
350
+ file,
351
+ organization,
352
+ urlObject,
353
+ langRegion,
354
+ lang,
355
+ theme,
356
+ state,
357
+ resolveFile,
358
+ crud,
359
+ maxDepth = DEFAULT_MAX_DEPTH
360
+ }) {
361
+ let dep = [];
362
+ const dbCache = new Map();
363
+ const stylesheetLinks = [];
364
+ const activeState = parseState(state);
365
+
366
+ // Accumulate translations from root and recursively included files
367
+ let activeTranslations = {};
368
+ if (file?.translations) {
369
+ mergeTranslationSource(activeTranslations, file.translations, langRegion, lang);
370
+ }
371
+
372
+ const organization_id = file?.organization_id;
373
+ const host = urlObject?.hostname;
374
+ const rootFilePath = file?.path || file?.pathname || "/";
375
+ let rootHtml = file?.src || "";
376
+
377
+ async function renderRecursive(currentHtml, lastKey, currentFilePath) {
378
+ // If maxDepth is 0, depth limiting is disabled and allowed to scale infinitely
379
+ if (maxDepth > 0 && dep.length > maxDepth) {
380
+ throw new Error(`Maximum recursive depth of ${maxDepth} exceeded in SSR rendering chain.`);
381
+ }
382
+
383
+ let doc = new HTMLScanner(currentHtml);
384
+ applyState(doc, activeState);
385
+ doc = new HTMLScanner(doc.toString());
386
+ const targets = doc.querySelectorAll({
387
+ attributes: ["src", "array"],
388
+ excludeTags: ["SCRIPT", "IMG", "IFRAME", "AUDIO", "VIDEO", "SOURCE", "TRACK", "INPUT", "EMBED", "FRAME"]
389
+ });
390
+
391
+ for (const el of targets) {
392
+ let chunkStr = null;
393
+ try {
394
+ const isSrc = el.hasAttribute("src") && !SKIP_SRC_ELEMENTS.has(el.tagName);
395
+ const isData = el.hasAttribute("array");
396
+
397
+ // 1. Process File Source [src]
398
+ if (isSrc) {
399
+ let src = el.getAttribute("src");
400
+ if (src && typeof src === "string") {
401
+ let pathname = currentFilePath;
402
+ if (!pathname.endsWith("/")) pathname += "/";
403
+
404
+ pathname = new URL(src, `http://localhost${pathname}`).pathname;
405
+ if (pathname.endsWith("/")) pathname += "index.html";
406
+
407
+ const srcKey = `file:${pathname}`;
408
+ if (dep.includes(srcKey)) {
409
+ throw new Error(`Infinite loop detected: Parent context "${lastKey}" requested "${srcKey}", which is already in active chain: ${dep.join(" -> ")}`);
410
+ }
411
+
412
+ dep.push(srcKey);
413
+ try {
414
+ let fetchedFile = null;
415
+ if (typeof resolveFile === "function") {
416
+ fetchedFile = await resolveFile(pathname);
417
+ } else if (crud) {
418
+ const data = await crud.send({
419
+ method: "object.read",
420
+ host,
421
+ array: "files",
422
+ object: "",
423
+ filter: { query: { pathname } },
424
+ state,
425
+ organization_id
426
+ });
427
+ fetchedFile = data?.object?.[0] || null;
428
+ const cacheControl = Object.entries(fetchedFile?.headers || {})
429
+ .find(([name]) => name.toLowerCase() === "cache-control")?.[1];
430
+ const publicFile = typeof cacheControl === "string" &&
431
+ /(?:^|,)\s*public\s*(?:,|$)/i.test(cacheControl) &&
432
+ !/(?:^|,)\s*(?:private|must-revalidate)\b/i.test(cacheControl);
433
+ if (fetchedFile && !publicFile) fetchedFile = { status: 403 };
434
+
435
+ }
436
+
437
+ let statusCode = fetchedFile?.status;
438
+ if (!statusCode) {
439
+ if (!fetchedFile || !fetchedFile.src) statusCode = 404;
440
+ else statusCode = 200;
441
+ }
442
+
443
+ if (statusCode === 304) {
444
+ el.setAttribute("error", "304");
445
+ el.removeAttribute("rendered");
446
+ continue;
447
+ }
448
+
449
+ if (statusCode >= 400) {
450
+ el.setAttribute("error", String(statusCode));
451
+ }
452
+
453
+ if (statusCode < 400 || el.hasAttribute("render-error")) {
454
+ if (fetchedFile?.translations) {
455
+ mergeTranslationSource(activeTranslations, fetchedFile.translations, langRegion, lang);
456
+ }
457
+
458
+ let val = fetchedFile?.src;
459
+ if (typeof val === "string" && val.startsWith("data:image/svg+xml;base64,")) {
460
+ val = Buffer.from(val.split(",")[1], "base64").toString("utf-8");
461
+ }
462
+ if (val !== undefined && val !== null) {
463
+ chunkStr = transformChunk(el, val);
464
+ const stylesheets = extractStylesheetLinks(chunkStr, rootHtml);
465
+ stylesheetLinks.push(...stylesheets.links);
466
+ chunkStr = stylesheets.cleanHtml;
467
+ const merged = extractAndMergeHead(rootHtml, chunkStr);
468
+ rootHtml = merged.rootHtml;
469
+ chunkStr = await renderRecursive(merged.cleanChunkHtml, srcKey, pathname);
470
+ }
471
+ }
472
+ } finally {
473
+ dep.pop();
474
+ }
475
+ }
476
+ }
477
+ // 2. Process Data Source [array]
478
+ else if (isData) {
479
+ let shouldProcess = true;
480
+
481
+ if (
482
+ IGNORE_ELEMENTS.has(el.tagName) ||
483
+ !el.hasAttribute("key")
484
+ ) {
485
+ shouldProcess = false;
486
+ } else if (el.hasAttribute("render-query") || el.hasAttribute("component") || el.hasAttribute("plugin") || el.hasAttribute("actions")) {
487
+ shouldProcess = false;
488
+ }
489
+
490
+ if (shouldProcess && crud) {
491
+ const array = el.getAttribute("array");
492
+ const key = el.getAttribute("key");
493
+ const _id = el.getAttribute("object") || "";
494
+ const filterAttr = el.getAttribute("filter");
495
+ let filter = {};
496
+
497
+ if (filterAttr) {
498
+ try { filter = JSON.parse(filterAttr); } catch (e) { }
499
+ }
500
+
501
+ if (array && checkValue(array) && (!key || checkValue(key))) {
502
+ const keyStr = key ? `-${key}` : "";
503
+ const filterKeyStr = filterAttr ? `-${filterAttr}` : "";
504
+ const crudKey = `data:${_id}${array}${keyStr}${filterKeyStr}`;
505
+
506
+ if (dep.includes(crudKey)) {
507
+ throw new Error(`Infinite loop detected: Parent context "${lastKey}" requested data key "${crudKey}", which is already in active chain: ${dep.join(" -> ")}`);
508
+ }
509
+
510
+ dep.push(crudKey);
511
+ try {
512
+ const cacheKey = `${_id}${array}${filterKeyStr}`;
513
+ let data;
514
+
515
+ if (dbCache.has(cacheKey)) {
516
+ data = dbCache.get(cacheKey);
517
+ } else {
518
+ data = await crud.send({
519
+ method: "object.read",
520
+ host,
521
+ array,
522
+ object: _id ? { _id } : "",
523
+ filter,
524
+ state,
525
+ organization_id
526
+ });
527
+ if (data?.object?.[0]) data = data.object[0];
528
+ dbCache.set(cacheKey, data);
529
+ }
530
+
531
+ if (data) {
532
+ const chunkVal = key ? data[key] : data;
533
+ if (chunkVal !== undefined && chunkVal !== null) {
534
+ const rawChunk = transformChunk(el, chunkVal);
535
+ const stylesheets = extractStylesheetLinks(rawChunk, rootHtml);
536
+ stylesheetLinks.push(...stylesheets.links);
537
+ const merged = extractAndMergeHead(rootHtml, stylesheets.cleanHtml);
538
+ rootHtml = merged.rootHtml;
539
+ chunkStr = await renderRecursive(merged.cleanChunkHtml, crudKey, currentFilePath);
540
+ }
541
+ }
542
+ } finally {
543
+ dep.pop();
544
+ }
545
+ }
546
+ }
547
+ }
548
+
549
+ if (chunkStr !== null) {
550
+ if (getValueAttribute(el, "type", "set-value")?.toLowerCase() === "outerhtml") {
551
+ el.replaceWith(chunkStr);
552
+ } else {
553
+ el.setAttribute("rendered", "true");
554
+ el.setInnerHTML(chunkStr);
555
+ }
556
+ }
557
+ } catch (error) {
558
+ const statusCode = Number(error?.statusCode || error?.status);
559
+ el.setAttribute(
560
+ "error",
561
+ String(statusCode >= 400 && statusCode < 600 ? statusCode : 500)
562
+ );
563
+ console.error("SSR element rendering failed:", error);
564
+ }
565
+ }
566
+
567
+ return doc.toString();
568
+ }
569
+
570
+ const rootNormalizedPath = rootFilePath.endsWith("/") ? `${rootFilePath}index.html` : rootFilePath;
571
+ dep.push(`file:${rootNormalizedPath}`);
572
+
573
+ try {
574
+ rootHtml = await renderRecursive(rootHtml, "root", rootNormalizedPath);
575
+
576
+ const rootStylesheets = extractStylesheetLinks(rootHtml);
577
+ rootHtml = mergeStylesheetLinks(
578
+ rootStylesheets.cleanHtml,
579
+ [...rootStylesheets.links, ...stylesheetLinks]
580
+ );
581
+
582
+ // Apply translations to full composite string
583
+ if (langRegion || lang) {
584
+ rootHtml = translate(rootHtml, activeTranslations, langRegion, lang);
585
+ }
586
+
587
+ // Apply visual theme
588
+ rootHtml = applyTheme(rootHtml, theme);
589
+
590
+ // Inject localization alternate links into <head>
591
+ if (organization?.languages?.length > 0) {
592
+ const langLinks = createLanguageLinkTags(file, organization, urlObject);
593
+ if (langLinks) {
594
+ const doc = new HTMLScanner(rootHtml);
595
+ const headHandles = doc.querySelectorAll({ tags: ["HEAD"] });
596
+ if (headHandles.length > 0) {
597
+ const headContent = headHandles[0].getInnerHTML();
598
+ headHandles[0].setInnerHTML(`${headContent}\n${langLinks}`);
599
+ rootHtml = doc.toString();
600
+ }
601
+ }
602
+ }
603
+ } finally {
604
+ dep = [];
605
+ dbCache.clear();
606
+ }
607
+
608
+ // Replace ObjectId() tokens across final rendered document
609
+ return rootHtml.replaceAll("ObjectId()", () => ObjectId().toString());
610
+ }
611
+
612
+ /**
613
+ *
614
+ * @name actions
615
+ * @kind attribute
616
+ * @environment server
617
+ */
618
+
619
+ /**
620
+ *
621
+ * @name array
622
+ * @kind attribute
623
+ * @environment server
624
+ */
625
+
626
+ /**
627
+ *
628
+ * @name class
629
+ * @kind attribute
630
+ * @environment server
631
+ */
632
+
633
+ /**
634
+ *
635
+ * @name component
636
+ * @kind attribute
637
+ * @environment server
638
+ */
639
+
640
+ /**
641
+ *
642
+ * @name content
643
+ * @kind attribute
644
+ * @environment server
645
+ */
646
+
647
+ /**
648
+ *
649
+ * @name color-scheme
650
+ * @kind attribute
651
+ * @environment server
652
+ */
653
+
654
+ /**
655
+ *
656
+ * @name error
657
+ * @kind attribute
658
+ * @environment server
659
+ */
660
+
661
+ /**
662
+ *
663
+ * @name filter
664
+ * @kind attribute
665
+ * @environment server
666
+ */
667
+
668
+ /**
669
+ *
670
+ * @name href
671
+ * @kind attribute
672
+ * @environment server
673
+ */
674
+
675
+ /**
676
+ *
677
+ * @name i18n
678
+ * @kind attribute
679
+ * @environment server
680
+ */
681
+
682
+ /**
683
+ *
684
+ * @name key
685
+ * @kind attribute
686
+ * @environment server
687
+ */
688
+
689
+ /**
690
+ *
691
+ * @name media
692
+ * @kind attribute
693
+ * @environment server
694
+ */
695
+
696
+ /**
697
+ *
698
+ * @name name
699
+ * @kind attribute
700
+ * @environment server
701
+ */
702
+
703
+ /**
704
+ *
705
+ * @name object
706
+ * @kind attribute
707
+ * @environment server
708
+ */
709
+
710
+ /**
711
+ *
712
+ * @name plugin
713
+ * @kind attribute
714
+ * @environment server
715
+ */
716
+
717
+ /**
718
+ *
719
+ * @name rel
720
+ * @kind attribute
721
+ * @environment server
722
+ */
723
+
724
+ /**
725
+ *
726
+ * @name render-error
727
+ * @kind attribute
728
+ * @environment server
729
+ */
730
+
731
+ /**
732
+ *
733
+ * @name render-query
734
+ * @kind attribute
735
+ * @environment server
736
+ */
737
+
738
+ /**
739
+ *
740
+ * @name rendered
741
+ * @kind attribute
742
+ * @environment server
743
+ */
744
+
745
+ /**
746
+ *
747
+ * @name src
748
+ * @kind attribute
749
+ * @environment server
750
+ */
751
+
752
+ /**
753
+ *
754
+ * @name state-id
755
+ * @kind attribute
756
+ * @environment server
757
+ */
758
+
759
+ /**
760
+ *
761
+ * @name state-overwrite
762
+ * @kind attribute
763
+ * @environment server
764
+ */
765
+
766
+ /**
767
+ *
768
+ * @name type
769
+ * @kind attribute
770
+ * @environment server
771
+ */
772
+
773
+ /**
774
+ * High-performance, stateless server-side rendering (SSR) engine with recursive HTML template parsing, multi-source data resolution, and automated circular dependency protection.
775
+ *
776
+ * @module render
777
+ * @environment server
778
+ */