Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions packages/markdown/src/plugins/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { Batch, definePlugin, IInstance, Plugin } from '../factory.js';
import { sanitizedHTML } from '../sanitize.js';
import { getJsonScriptTag } from './util.js';

interface CheckboxInstance {
id: string;
Expand All @@ -22,17 +23,17 @@ export const checkboxPlugin: Plugin = {
name: 'checkbox',
initializePlugin: (md) => definePlugin(md, 'checkbox'),
fence: (token, idx) => {
const CheckboxId = `Checkbox-${idx}`;
return sanitizedHTML('div', { id: CheckboxId, class: 'checkbox' }, token.content.trim());
return sanitizedHTML('div', { class: 'checkbox' }, token.content.trim(), true);
},
hydrateComponent: async (renderer, errorHandler) => {
const checkboxInstances: CheckboxInstance[] = [];
const containers = renderer.element.querySelectorAll('.checkbox');
for (const [index, container] of Array.from(containers).entries()) {
if (!container.textContent) continue;
const scriptTag = getJsonScriptTag(container);
if (!scriptTag) continue;

try {
const spec: CheckboxSpec = JSON.parse(container.textContent);
const spec: CheckboxSpec = JSON.parse(scriptTag.textContent);

const html = `<form class="vega-bindings">
<div class="vega-bind">
Expand All @@ -45,7 +46,7 @@ export const checkboxPlugin: Plugin = {
container.innerHTML = html;
const element = container.querySelector('input[type="checkbox"]') as HTMLInputElement;

const checkboxInstance: CheckboxInstance = { id: container.id, spec, element };
const checkboxInstance: CheckboxInstance = { id: `checkbox-${index}`, spec, element };
checkboxInstances.push(checkboxInstance);
} catch (e) {
container.innerHTML = `<div class="error">${e.toString()}</div>`;
Expand Down
13 changes: 7 additions & 6 deletions packages/markdown/src/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { definePlugin, IInstance, Plugin } from '../factory.js';
import { sanitizedHTML } from '../sanitize.js';
import * as Csstree from 'css-tree';
import { getJsonScriptTag } from './util.js';

// CSS Tree is expected to be available as a global variable
declare const csstree: typeof Csstree;
Expand Down Expand Up @@ -345,13 +346,12 @@ export const cssPlugin: Plugin = {
const info = token.info.trim();

if (info === 'css') {
const cssId = `css-${idx}`;
const cssContent = token.content.trim();

// Parse and categorize CSS content
const categorizedCss = categorizeCss(cssContent);

return sanitizedHTML('div', { id: cssId, class: 'css-component' }, JSON.stringify(categorizedCss));
return sanitizedHTML('div', { class: 'css-component' }, JSON.stringify(categorizedCss), true);
}

// Fallback to original fence renderer
Expand All @@ -367,10 +367,11 @@ export const cssPlugin: Plugin = {
const containers = renderer.element.querySelectorAll('.css-component');

for (const [index, container] of Array.from(containers).entries()) {
if (!container.textContent) continue;
const scriptTag = getJsonScriptTag(container);
if (!scriptTag) continue;

try {
const categorizedCss: CategorizedCss = JSON.parse(container.textContent);
const categorizedCss: CategorizedCss = JSON.parse(scriptTag.textContent);
const comments: string[] = [];

// Log security issues found
Expand All @@ -385,7 +386,7 @@ export const cssPlugin: Plugin = {
if (safeCss.trim().length > 0) {
const styleElement = document.createElement('style');
styleElement.type = 'text/css';
styleElement.id = `idocs-css-${container.id}`;
styleElement.id = `idocs-css-${index}`;
styleElement.textContent = safeCss;

// Apply to shadow DOM if available, otherwise document
Expand All @@ -395,7 +396,7 @@ export const cssPlugin: Plugin = {
comments.push(`<!-- CSS styles applied to ${renderer.shadowRoot ? 'shadow DOM' : 'document'} -->`);

cssInstances.push({
id: container.id,
id: `css-${index}`,
element: styleElement
});
} else {
Expand Down
11 changes: 6 additions & 5 deletions packages/markdown/src/plugins/dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import { Batch, definePlugin, IInstance, Plugin } from '../factory.js';
import { sanitizedHTML } from '../sanitize.js';
import { getJsonScriptTag } from './util.js';

interface DropdownInstance {
id: string;
Expand Down Expand Up @@ -32,17 +33,17 @@ export const dropdownPlugin: Plugin = {
name: 'dropdown',
initializePlugin: (md) => definePlugin(md, 'dropdown'),
fence: (token, idx) => {
const DropdownId = `Dropdown-${idx}`;
return sanitizedHTML('div', { id: DropdownId, class: 'dropdown' }, token.content.trim());
return sanitizedHTML('div', { class: 'dropdown' }, token.content.trim(), true);
},
hydrateComponent: async (renderer, errorHandler) => {
const dropdownInstances: DropdownInstance[] = [];
const containers = renderer.element.querySelectorAll('.dropdown');
for (const [index, container] of Array.from(containers).entries()) {
if (!container.textContent) continue;
const scriptTag = getJsonScriptTag(container);
if (!scriptTag) continue;

try {
const spec: DropdownSpec = JSON.parse(container.textContent);
const spec: DropdownSpec = JSON.parse(scriptTag.textContent);

const html = `<form class="vega-bindings">
<div class="vega-bind">
Expand All @@ -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 = `<div class="error">${e.toString()}</div>`;
Expand Down
11 changes: 6 additions & 5 deletions packages/markdown/src/plugins/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = `
Expand Down Expand Up @@ -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 = `<div class="error">${e.toString()}</div>`;
Expand Down
11 changes: 6 additions & 5 deletions packages/markdown/src/plugins/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 = `<div class="error">${e.toString()}</div>`;
errorHandler(e, 'presets', index, 'parse', container);
Expand Down
11 changes: 6 additions & 5 deletions packages/markdown/src/plugins/tabulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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;
Expand Down
11 changes: 11 additions & 0 deletions packages/markdown/src/plugins/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
18 changes: 15 additions & 3 deletions packages/markdown/src/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand Down
Loading