@cocreate/render 1.51.0 → 2.0.0

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,801 @@
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 member
616
+ * @attribute
617
+ * @environment server
618
+ */
619
+
620
+ /**
621
+ *
622
+ * @name array
623
+ * @kind member
624
+ * @attribute
625
+ * @environment server
626
+ */
627
+
628
+ /**
629
+ *
630
+ * @name class
631
+ * @kind member
632
+ * @attribute
633
+ * @environment server
634
+ */
635
+
636
+ /**
637
+ *
638
+ * @name component
639
+ * @kind member
640
+ * @attribute
641
+ * @environment server
642
+ */
643
+
644
+ /**
645
+ *
646
+ * @name content
647
+ * @kind member
648
+ * @attribute
649
+ * @environment server
650
+ */
651
+
652
+ /**
653
+ *
654
+ * @name color-scheme
655
+ * @kind member
656
+ * @attribute
657
+ * @environment server
658
+ */
659
+
660
+ /**
661
+ *
662
+ * @name error
663
+ * @kind member
664
+ * @attribute
665
+ * @environment server
666
+ */
667
+
668
+ /**
669
+ *
670
+ * @name filter
671
+ * @kind member
672
+ * @attribute
673
+ * @environment server
674
+ */
675
+
676
+ /**
677
+ *
678
+ * @name href
679
+ * @kind member
680
+ * @attribute
681
+ * @environment server
682
+ */
683
+
684
+ /**
685
+ *
686
+ * @name i18n
687
+ * @kind member
688
+ * @attribute
689
+ * @environment server
690
+ */
691
+
692
+ /**
693
+ *
694
+ * @name key
695
+ * @kind member
696
+ * @attribute
697
+ * @environment server
698
+ */
699
+
700
+ /**
701
+ *
702
+ * @name media
703
+ * @kind member
704
+ * @attribute
705
+ * @environment server
706
+ */
707
+
708
+ /**
709
+ *
710
+ * @name name
711
+ * @kind member
712
+ * @attribute
713
+ * @environment server
714
+ */
715
+
716
+ /**
717
+ *
718
+ * @name object
719
+ * @kind member
720
+ * @attribute
721
+ * @environment server
722
+ */
723
+
724
+ /**
725
+ *
726
+ * @name plugin
727
+ * @kind member
728
+ * @attribute
729
+ * @environment server
730
+ */
731
+
732
+ /**
733
+ *
734
+ * @name rel
735
+ * @kind member
736
+ * @attribute
737
+ * @environment server
738
+ */
739
+
740
+ /**
741
+ *
742
+ * @name render-error
743
+ * @kind member
744
+ * @attribute
745
+ * @environment server
746
+ */
747
+
748
+ /**
749
+ *
750
+ * @name render-query
751
+ * @kind member
752
+ * @attribute
753
+ * @environment server
754
+ */
755
+
756
+ /**
757
+ *
758
+ * @name rendered
759
+ * @kind member
760
+ * @attribute
761
+ * @environment server
762
+ */
763
+
764
+ /**
765
+ *
766
+ * @name src
767
+ * @kind member
768
+ * @attribute
769
+ * @environment server
770
+ */
771
+
772
+ /**
773
+ *
774
+ * @name state-id
775
+ * @kind member
776
+ * @attribute
777
+ * @environment server
778
+ */
779
+
780
+ /**
781
+ *
782
+ * @name state-overwrite
783
+ * @kind member
784
+ * @attribute
785
+ * @environment server
786
+ */
787
+
788
+ /**
789
+ *
790
+ * @name type
791
+ * @kind member
792
+ * @attribute
793
+ * @environment server
794
+ */
795
+
796
+ /**
797
+ * High-performance, stateless server-side rendering (SSR) engine with recursive HTML template parsing, multi-source data resolution, and automated circular dependency protection.
798
+ *
799
+ * @module render
800
+ * @environment server
801
+ */