@@ -59,7 +60,7 @@ export const dropdownPlugin: Plugin = {
// Safely set the initial options
setSelectOptions(element, spec.multiple ?? false, spec.options ?? [], spec.value ?? (spec.multiple ? [] : ""));
- const dropdownInstance: DropdownInstance = { id: container.id, spec, element };
+ const dropdownInstance: DropdownInstance = { id: `dropdown-${index}`, spec, element };
dropdownInstances.push(dropdownInstance);
} catch (e) {
container.innerHTML = `
${e.toString()}
`;
diff --git a/packages/markdown/src/plugins/image.ts b/packages/markdown/src/plugins/image.ts
index 50ed26ec..2dd677e0 100644
--- a/packages/markdown/src/plugins/image.ts
+++ b/packages/markdown/src/plugins/image.ts
@@ -5,6 +5,7 @@
import { definePlugin, IInstance, Plugin } from '../factory.js';
import { sanitizedHTML } from '../sanitize.js';
+import { getJsonScriptTag } from './util.js';
export interface ImageSpec {
srcSignalName: string;
@@ -30,16 +31,16 @@ export const imagePlugin: Plugin = {
name: 'image',
initializePlugin: (md) => definePlugin(md, 'image'),
fence: (token, idx) => {
- const ImageId = `Image-${idx}`;
- return sanitizedHTML('div', { id: ImageId, class: 'image' }, token.content.trim());
+ return sanitizedHTML('div', { class: 'image' }, token.content.trim(), true);
},
hydrateComponent: async (renderer, errorHandler) => {
const imageInstances: ImageInstance[] = [];
const containers = renderer.element.querySelectorAll('.image');
for (const [index, container] of Array.from(containers).entries()) {
- if (!container.textContent) continue;
+ const scriptTag = getJsonScriptTag(container);
+ if (!scriptTag) continue;
try {
- const spec: ImageSpec = JSON.parse(container.textContent);
+ const spec: ImageSpec = JSON.parse(scriptTag.textContent);
const element = document.createElement('img');
const spinner = document.createElement('div');
spinner.innerHTML = `
@@ -68,7 +69,7 @@ export const imagePlugin: Plugin = {
container.appendChild(spinner);
container.appendChild(element);
- const imageInstance: ImageInstance = { id: container.id, spec, element, spinner };
+ const imageInstance: ImageInstance = { id: `image-${index}`, spec, element, spinner };
imageInstances.push(imageInstance);
} catch (e) {
container.innerHTML = `
${e.toString()}
`;
diff --git a/packages/markdown/src/plugins/presets.ts b/packages/markdown/src/plugins/presets.ts
index dbbf6668..25fa1522 100644
--- a/packages/markdown/src/plugins/presets.ts
+++ b/packages/markdown/src/plugins/presets.ts
@@ -5,6 +5,7 @@
import { Batch, definePlugin, IInstance, Plugin, PrioritizedSignal } from '../factory.js';
import { sanitizedHTML } from '../sanitize.js';
+import { getJsonScriptTag } from './util.js';
interface Preset {
name: string;
@@ -25,19 +26,19 @@ export const presetsPlugin: Plugin = {
initializePlugin: (md) => definePlugin(md, 'presets'),
fence: (token, idx) => {
const spec = JSON.parse(token.content.trim());
- const pluginId = `preset-${idx}`;
- return sanitizedHTML('div', { id: pluginId, class: 'presets' }, JSON.stringify(spec));
+ return sanitizedHTML('div', { class: 'presets' }, JSON.stringify(spec), true);
},
hydrateComponent: async (renderer, errorHandler) => {
const presetsInstances: PresetsInstance[] = [];
const containers = renderer.element.querySelectorAll('.presets');
for (const [index, container] of Array.from(containers).entries()) {
- if (!container.textContent) continue;
+ const scriptTag = getJsonScriptTag(container);
+ if (!scriptTag) continue;
- const id = `presets${index}`;
+ const id = `presets-${index}`;
let presets: Preset[];
try {
- presets = JSON.parse(container.textContent) as Preset[];
+ presets = JSON.parse(scriptTag.textContent) as Preset[];
} catch (e) {
container.innerHTML = `
${e.toString()}
`;
errorHandler(e, 'presets', index, 'parse', container);
diff --git a/packages/markdown/src/plugins/tabulator.ts b/packages/markdown/src/plugins/tabulator.ts
index 69972b06..9f633991 100644
--- a/packages/markdown/src/plugins/tabulator.ts
+++ b/packages/markdown/src/plugins/tabulator.ts
@@ -7,6 +7,7 @@ import { defaultCommonOptions } from 'common';
import { Batch, definePlugin, IInstance, Plugin } from '../factory.js';
import { sanitizedHTML } from '../sanitize.js';
import { Tabulator as TabulatorType, Options as TabulatorOptions } from 'tabulator-tables';
+import { getJsonScriptTag } from './util.js';
interface TabulatorInstance {
id: string;
@@ -26,21 +27,21 @@ export const tabulatorPlugin: Plugin = {
name: 'tabulator',
initializePlugin: (md) => definePlugin(md, 'tabulator'),
fence: (token, idx) => {
- const tabulatorId = `tabulator-${idx}`;
- return sanitizedHTML('div', { id: tabulatorId, class: 'tabulator', style: 'box-sizing: border-box;' }, token.content.trim());
+ return sanitizedHTML('div', { class: 'tabulator', style: 'box-sizing: border-box;' }, token.content.trim(), true);
},
hydrateComponent: async (renderer, errorHandler) => {
const tabulatorInstances: TabulatorInstance[] = [];
const containers = renderer.element.querySelectorAll('.tabulator');
for (const [index, container] of Array.from(containers).entries()) {
- if (!container.textContent) continue;
+ const scriptTag = getJsonScriptTag(container);
+ if (!scriptTag) continue;
if (!Tabulator) {
errorHandler(new Error('Tabulator not found'), 'tabulator', index, 'init', container);
continue;
}
try {
- const spec: TabulatorSpec = JSON.parse(container.textContent);
+ const spec: TabulatorSpec = JSON.parse(scriptTag.textContent);
let options: TabulatorOptions = {
autoColumns: true,
@@ -54,7 +55,7 @@ export const tabulatorPlugin: Plugin = {
}
const table = new Tabulator(container as HTMLElement, options);
- const tabulatorInstance: TabulatorInstance = { id: container.id, spec, table, built: false };
+ const tabulatorInstance: TabulatorInstance = { id: `tabulator-${index}`, spec, table, built: false };
table.on('tableBuilt', () => {
table.off('tableBuilt');
tabulatorInstance.built = true;
diff --git a/packages/markdown/src/plugins/util.ts b/packages/markdown/src/plugins/util.ts
index a67ac240..31e2c537 100644
--- a/packages/markdown/src/plugins/util.ts
+++ b/packages/markdown/src/plugins/util.ts
@@ -6,3 +6,14 @@ export function urlParam(urlParamName: string, value: any) {
return `${urlParamName}=${encodeURIComponent(value)}`;
}
}
+
+export function getJsonScriptTag(container: Element): HTMLScriptElement | null {
+ const scriptTag = container.previousElementSibling;
+ if (scriptTag?.tagName !== 'SCRIPT' || scriptTag.getAttribute('type') !== 'application/json') {
+ return null;
+ }
+ if (!scriptTag.textContent) {
+ return null;
+ }
+ return scriptTag as HTMLScriptElement;
+}
diff --git a/packages/markdown/src/sanitize.ts b/packages/markdown/src/sanitize.ts
index 6499a926..42097c7a 100644
--- a/packages/markdown/src/sanitize.ts
+++ b/packages/markdown/src/sanitize.ts
@@ -3,7 +3,7 @@
* Licensed under the MIT License.
*/
-export function sanitizedHTML(tagName: string, attributes: { [key: string]: string }, content: string) {
+export function sanitizedHTML(tagName: string, attributes: { [key: string]: string }, content: string, precedeWithScriptTag?: boolean) {
// Create a temp element with the specified tag name
const element = document.createElement(tagName);
@@ -13,8 +13,20 @@ export function sanitizedHTML(tagName: string, attributes: { [key: string]: stri
element.setAttribute(key, attributes[key]);
});
- // Set the textContent to automatically escape the content
- element.textContent = content;
+ if (precedeWithScriptTag) {
+ // Create a script tag that precedes the main element
+ const scriptElement = document.createElement('script');
+ scriptElement.setAttribute('type', 'application/json');
+ // Only escape the dangerous sequence that could break out of script tag
+ const safeContent = content.replace(/<\/script>/gi, '<\\/script>');
+ scriptElement.innerHTML = safeContent;
+
+ // Return script tag followed by empty element
+ return scriptElement.outerHTML + element.outerHTML;
+ } else {
+ // Set the textContent to automatically escape the content
+ element.textContent = content;
+ }
// Return the outer HTML of the element
return element.outerHTML;