From e090d64bfdbcd68f185a896001a77f5a22e8b1ad Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 12:10:57 -0700 Subject: [PATCH 01/20] config container --- packages/markdown/src/factory.ts | 40 ++++++++++++++++-- packages/markdown/src/plugins/config.ts | 47 +++++++++++++++++++++ packages/markdown/src/plugins/css.ts | 49 +++++++++++----------- packages/markdown/src/plugins/util.ts | 2 + packages/markdown/src/plugins/vega-lite.ts | 9 ++-- packages/markdown/src/plugins/vega.ts | 31 +++++++------- packages/markdown/src/renderer.ts | 22 ++++------ 7 files changed, 141 insertions(+), 59 deletions(-) create mode 100644 packages/markdown/src/plugins/config.ts diff --git a/packages/markdown/src/factory.ts b/packages/markdown/src/factory.ts index 5f41e580..6d2e76a8 100644 --- a/packages/markdown/src/factory.ts +++ b/packages/markdown/src/factory.ts @@ -37,18 +37,52 @@ export interface IInstance { getCurrentSignalValue?: (signalName: string) => unknown; } -export interface Plugin { +export interface IConfig { + spec: T; + hasFlags?: boolean; + reason?: string; +} + +export interface IConfigContainer { + container: HTMLElement; + config: IConfig; +} + +export interface Plugin { name: string; hydratesBefore?: string; initializePlugin: (md: MarkdownIt) => void; fence?: (token: Token, idx: number) => string; - hydrateComponent?: (renderer: Renderer, errorHandler: ErrorHandler) => Promise; + hydrateConfig?: (renderer: Renderer, errorHandler: ErrorHandler) => IConfigContainer[]; + hydrateComponent?: (renderer: Renderer, errorHandler: ErrorHandler, configs?: IConfigContainer[]) => Promise; } export const plugins: Plugin[] = []; export function registerMarkdownPlugin(plugin: Plugin) { - plugins.push(plugin); + // Find the correct position to insert the plugin based on hydratesBefore + let insertIndex = plugins.length; + + // First, find the latest position where plugins that should run before this one are located + let minIndex = 0; + for (let i = 0; i < plugins.length; i++) { + if (plugins[i].hydratesBefore === plugin.name) { + minIndex = Math.max(minIndex, i + 1); + } + } + + // Then, if this plugin should run before another plugin, find that plugin's position + if (plugin.hydratesBefore) { + const targetIndex = plugins.findIndex(p => p.name === plugin.hydratesBefore); + if (targetIndex !== -1) { + insertIndex = targetIndex; + } + } + + // Ensure we don't insert before plugins that should run before this one + insertIndex = Math.max(insertIndex, minIndex); + + plugins.splice(insertIndex, 0, plugin); return 'register'; } diff --git a/packages/markdown/src/plugins/config.ts b/packages/markdown/src/plugins/config.ts new file mode 100644 index 00000000..09f6842e --- /dev/null +++ b/packages/markdown/src/plugins/config.ts @@ -0,0 +1,47 @@ +import { definePlugin, IConfig, IConfigContainer, Plugin } from "../factory.js"; +import { ErrorHandler, Renderer } from '../renderer.js'; +import { sanitizedHTML } from "../sanitize.js"; +import { getJsonScriptTag } from "./util.js"; + +export function hydrateConfig(pluginName: string, className: string, renderer: Renderer, errorHandler: ErrorHandler) { + const configs: IConfigContainer[] = []; + const containers = renderer.element.querySelectorAll(`.${className}`); + for (const [index, container] of Array.from(containers).entries()) { + const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); + if (!jsonObj) continue; + configs.push({ container: container as HTMLElement, config: jsonObj }); + } + return configs; +} + +export function configPlugin(pluginName: string, className: string, j2c?: (spec: T) => IConfig) { + const basePlugin: Plugin = { + name: pluginName, + initializePlugin: (md) => definePlugin(md, pluginName), + fence: token => { + let content = token.content.trim(); + if (j2c) { + let spec: T; + try { + spec = JSON.parse(content); + } catch (e) { + const result: IConfig = { + spec: null, + hasFlags: true, + reason: `malformed JSON` + }; + content = JSON.stringify(result); + } + if (spec) { + const config = j2c(spec); + content = JSON.stringify(config); + } + } + return sanitizedHTML('div', { class: className }, content, true); + }, + hydrateConfig: (renderer, errorHandler) => { + return hydrateConfig(pluginName, className, renderer, errorHandler); + }, + }; + return basePlugin; +} diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index 229373f5..7588d7d4 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. */ -import { definePlugin, IInstance, Plugin } from '../factory.js'; +import { definePlugin, IConfig, IConfigContainer, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import * as Csstree from 'css-tree'; import { getJsonScriptTag, pluginClassName } from './util.js'; +import { configPlugin, hydrateConfig } from './config.js'; // CSS Tree is expected to be available as a global variable declare const csstree: typeof Csstree; @@ -33,7 +34,6 @@ interface AtRule { interface CategorizedCss { atRules: { [atRuleSignature: string]: AtRule }; - hasFlags: boolean; } // Helper function to reconstitute an at-rule block from at-rule data @@ -83,9 +83,13 @@ function reconstituteCss(atRules: { [atRuleSignature: string]: AtRule }): string return cssBlocks.join('\n\n'); } -function categorizeCss(cssContent: string): CategorizedCss { - const result: CategorizedCss = { +function categorizeCss(cssContent: string) { + const spec: CategorizedCss = { atRules: {}, + }; + + const result: IConfig = { + spec, hasFlags: false }; @@ -183,17 +187,17 @@ function categorizeCss(cssContent: string): CategorizedCss { if (currentRule && currentRule.declarations.length > 0) { const targetAtRule = currentAtRuleSignature; - if (!result.atRules[targetAtRule]) { - result.atRules[targetAtRule] = { + if (!spec.atRules[targetAtRule]) { + spec.atRules[targetAtRule] = { signature: targetAtRule, rules: [] }; } - if (result.atRules[targetAtRule].rules) { - result.atRules[targetAtRule].rules.push(currentRule); + if (spec.atRules[targetAtRule].rules) { + spec.atRules[targetAtRule].rules.push(currentRule); } else { - result.atRules[targetAtRule].rules = [currentRule]; + spec.atRules[targetAtRule].rules = [currentRule]; } } } @@ -206,7 +210,7 @@ function categorizeCss(cssContent: string): CategorizedCss { // Check for @import specifically if (node.name === 'import') { const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, css: ruleContent, flag: 'importRule', @@ -220,7 +224,7 @@ function categorizeCss(cssContent: string): CategorizedCss { if (completeBlockAtRules.includes(node.name)) { // Store the entire rule as CSS and validate it as a complete block const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, css: ruleContent }; @@ -229,8 +233,8 @@ function categorizeCss(cssContent: string): CategorizedCss { // For other at-rules that contain rules (like @media, @supports) if (node.block) { - if (!result.atRules[atRuleSignature]) { - result.atRules[atRuleSignature] = { + if (!spec.atRules[atRuleSignature]) { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, rules: [] }; @@ -240,7 +244,7 @@ function categorizeCss(cssContent: string): CategorizedCss { } else { // Simple at-rule without block const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, css: ruleContent }; @@ -306,8 +310,8 @@ function categorizeCss(cssContent: string): CategorizedCss { const pluginName = 'css'; const className = pluginClassName(pluginName); -export const cssPlugin: Plugin = { - name: pluginName, +export const cssPlugin: Plugin = { + ...configPlugin(pluginName, className), initializePlugin: (md) => { // Check for required css-tree dependency if (typeof csstree === 'undefined') { @@ -365,19 +369,16 @@ export const cssPlugin: Plugin = { } }; }, - hydrateComponent: async (renderer, errorHandler) => { + hydrateComponent: async (renderer, errorHandler, configContainers) => { const cssInstances: { id: string; element: HTMLStyleElement }[] = []; - const containers = renderer.element.querySelectorAll(`.${className}`); - for (const [index, container] of Array.from(containers).entries()) { - const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); - if (!jsonObj) continue; + for (const [index, configContainer] of Array.from(configContainers).entries()) { - const categorizedCss: CategorizedCss = jsonObj; + const categorizedCss = configContainer.config.spec; const comments: string[] = []; // Log security issues found - if (categorizedCss.hasFlags) { + if (configContainer.config.hasFlags) { console.warn(`CSS security: Security issues detected in CSS`); comments.push(``); } @@ -404,7 +405,7 @@ export const cssPlugin: Plugin = { } else { comments.push(``); } - container.innerHTML = comments.join('\n'); + configContainer.container.innerHTML = comments.join('\n'); } const instances: IInstance[] = cssInstances.map((cssInstance) => { diff --git a/packages/markdown/src/plugins/util.ts b/packages/markdown/src/plugins/util.ts index 03524240..58fff829 100644 --- a/packages/markdown/src/plugins/util.ts +++ b/packages/markdown/src/plugins/util.ts @@ -10,9 +10,11 @@ export function urlParam(urlParamName: string, value: any) { export function getJsonScriptTag(container: Element, errorHandler: (error: Error) => void) { const scriptTag = container.previousElementSibling; if (scriptTag?.tagName !== 'SCRIPT' || scriptTag.getAttribute('type') !== 'application/json') { + errorHandler(new Error('Invalid JSON script tag')); return null; } if (!scriptTag.textContent) { + errorHandler(new Error('Empty JSON script tag')); return null; } try { diff --git a/packages/markdown/src/plugins/vega-lite.ts b/packages/markdown/src/plugins/vega-lite.ts index bca16d4a..30e062bf 100644 --- a/packages/markdown/src/plugins/vega-lite.ts +++ b/packages/markdown/src/plugins/vega-lite.ts @@ -3,10 +3,11 @@ * Licensed under the MIT License. */ -import { definePlugin, Plugin } from '../factory.js'; +import { Spec } from 'vega'; +import { definePlugin, IConfig, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { getJsonScriptTag, pluginClassName } from './util.js'; -import { vegaPlugin } from './vega.js'; +import { inspectSpec, vegaPlugin } from './vega.js'; import { compile } from 'vega-lite'; const pluginName = 'vega-lite'; @@ -29,8 +30,10 @@ export const vegaLitePlugin: Plugin = { try { const { spec } = compile(jsonObj); + const result = inspectSpec(spec); + //create both a script tag and a vega tag for the vega plugin to catch - const html = sanitizedHTML('div', { class: pluginClassName(vegaPlugin.name) }, JSON.stringify(spec, null, 2), true); + const html = sanitizedHTML('div', { class: pluginClassName(vegaPlugin.name) }, JSON.stringify(result, null, 2), true); //append the html right after this container container.insertAdjacentHTML('afterend', html); diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index cb4f503b..d8b89909 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -4,13 +4,13 @@ */ import { changeset, parse, View, expressionFunction, LoggerInterface } from 'vega'; -import { Batch, IInstance, Plugin, PrioritizedSignal, definePlugin } from '../factory.js'; -import { sanitizedHTML } from '../sanitize.js'; +import { Batch, IInstance, Plugin, PrioritizedSignal, IConfig } from '../factory.js'; import { BaseSignal, InitSignal, NewSignal, Runtime, Spec, ValuesData } from 'vega-typings'; import { ErrorHandler, Renderer } from '../renderer.js'; import { LogLevel } from '../signalbus.js'; -import { getJsonScriptTag, pluginClassName, urlParam } from './util.js'; +import { pluginClassName, urlParam } from './util.js'; import { defaultCommonOptions } from 'common'; +import { configPlugin } from './config.js'; const ignoredSignals = ['width', 'height', 'padding', 'autosize', 'background', 'style', 'parent', 'datum', 'item', 'event', 'cursor']; @@ -32,13 +32,17 @@ interface VegaInstance extends SpecInit { const pluginName = 'vega'; const className = pluginClassName(pluginName); -export const vegaPlugin: Plugin = { - name: pluginName, - initializePlugin: (md) => definePlugin(md, pluginName), - fence: token => { - return sanitizedHTML('div', { class: className }, token.content.trim(), true); - }, - hydrateComponent: async (renderer, errorHandler) => { +export function inspectSpec(spec: Spec) { + //TODO inspect spec for flags + const result: IConfig = { + spec, + }; + return result; +} + +export const vegaPlugin: Plugin = { + ...configPlugin(pluginName, className, inspectSpec), + hydrateComponent: async (renderer, errorHandler, configContainers) => { //initialize the expressionFunction only once if (!expressionsInitialized) { expressionFunction('urlParam', urlParam); @@ -46,13 +50,10 @@ export const vegaPlugin: Plugin = { } const vegaInstances: VegaInstance[] = []; - const containers = renderer.element.querySelectorAll(`.${className}`); const specInits: SpecInit[] = []; - for (const [index, container] of Array.from(containers).entries()) { - const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); - if (!jsonObj) continue; + for (const [index, configContainer] of Array.from(configContainers).entries()) { - const specInit = createSpecInit(container, index, jsonObj); + const specInit = createSpecInit(configContainer.container, index, configContainer.config.spec); if (specInit) { specInits.push(specInit); } diff --git a/packages/markdown/src/renderer.ts b/packages/markdown/src/renderer.ts index af1c5f3d..606436aa 100644 --- a/packages/markdown/src/renderer.ts +++ b/packages/markdown/src/renderer.ts @@ -5,7 +5,7 @@ import MarkdownIt from 'markdown-it'; import { Renderers } from 'vega-typings'; -import { create, IInstance, plugins } from './factory.js'; +import { create, IConfigContainer, IInstance, plugins } from './factory.js'; import { SignalBus } from './signalbus.js'; import { defaultCommonOptions } from 'common'; @@ -95,21 +95,15 @@ export class Renderer { this.signalBus.log('Renderer', 'rendering DOM'); const hydrationPromises: Promise[] = []; - //create a copy of the plugins and sort them by runsBefore - const sortedPlugins = [...plugins].sort((a, b) => { - // If plugin a should run before plugin b, a comes first - if (a.hydratesBefore === b.name) return -1; - // If plugin b should run before plugin a, b comes first - if (b.hydratesBefore === a.name) return 1; - // Otherwise maintain original order - return 0; - }); - - for (let i = 0; i < sortedPlugins.length; i++) { - const plugin = sortedPlugins[i]; + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + let configContainers: IConfigContainer<{}>[]; + if (plugin.hydrateConfig) { + configContainers = plugin.hydrateConfig(this, this.options.errorHandler); + } if (plugin.hydrateComponent) { //make a new promise that returns IInstances but adds the plugin name - hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler).then(instances => { + hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler, configContainers).then(instances => { return { pluginName: plugin.name, instances, From 4eafc9a2bd7fe98024aec9856bca63f6e735eb11 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 12:40:02 -0700 Subject: [PATCH 02/20] rename flaggables --- packages/markdown/src/factory.ts | 10 ++--- packages/markdown/src/plugins/config.ts | 49 ++++++++++------------ packages/markdown/src/plugins/css.ts | 14 +++---- packages/markdown/src/plugins/vega-lite.ts | 43 +++++++++---------- packages/markdown/src/plugins/vega.ts | 14 +++---- packages/markdown/src/renderer.ts | 8 ++-- 6 files changed, 68 insertions(+), 70 deletions(-) diff --git a/packages/markdown/src/factory.ts b/packages/markdown/src/factory.ts index 6d2e76a8..8411a5dd 100644 --- a/packages/markdown/src/factory.ts +++ b/packages/markdown/src/factory.ts @@ -37,15 +37,15 @@ export interface IInstance { getCurrentSignalValue?: (signalName: string) => unknown; } -export interface IConfig { +export interface FlaggableSpec { spec: T; hasFlags?: boolean; reason?: string; } -export interface IConfigContainer { +export interface SpecContainer { container: HTMLElement; - config: IConfig; + flaggableSpec: FlaggableSpec; } export interface Plugin { @@ -53,8 +53,8 @@ export interface Plugin { hydratesBefore?: string; initializePlugin: (md: MarkdownIt) => void; fence?: (token: Token, idx: number) => string; - hydrateConfig?: (renderer: Renderer, errorHandler: ErrorHandler) => IConfigContainer[]; - hydrateComponent?: (renderer: Renderer, errorHandler: ErrorHandler, configs?: IConfigContainer[]) => Promise; + hydrateSpecs?: (renderer: Renderer, errorHandler: ErrorHandler) => SpecContainer[]; + hydrateComponent?: (renderer: Renderer, errorHandler: ErrorHandler, specContainers?: SpecContainer[]) => Promise; } export const plugins: Plugin[] = []; diff --git a/packages/markdown/src/plugins/config.ts b/packages/markdown/src/plugins/config.ts index 09f6842e..b55fbae8 100644 --- a/packages/markdown/src/plugins/config.ts +++ b/packages/markdown/src/plugins/config.ts @@ -1,47 +1,44 @@ -import { definePlugin, IConfig, IConfigContainer, Plugin } from "../factory.js"; -import { ErrorHandler, Renderer } from '../renderer.js'; +import { definePlugin, FlaggableSpec, SpecContainer, Plugin } from "../factory.js"; import { sanitizedHTML } from "../sanitize.js"; import { getJsonScriptTag } from "./util.js"; -export function hydrateConfig(pluginName: string, className: string, renderer: Renderer, errorHandler: ErrorHandler) { - const configs: IConfigContainer[] = []; - const containers = renderer.element.querySelectorAll(`.${className}`); - for (const [index, container] of Array.from(containers).entries()) { - const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); - if (!jsonObj) continue; - configs.push({ container: container as HTMLElement, config: jsonObj }); - } - return configs; -} - -export function configPlugin(pluginName: string, className: string, j2c?: (spec: T) => IConfig) { - const basePlugin: Plugin = { +export function flaggableJsonPlugin(pluginName: string, className: string, flagger?: (spec: T) => FlaggableSpec) { + const plugin: Plugin = { name: pluginName, initializePlugin: (md) => definePlugin(md, pluginName), fence: token => { - let content = token.content.trim(); - if (j2c) { + let json = token.content.trim(); + if (flagger) { let spec: T; + let flaggableSpec: FlaggableSpec; try { - spec = JSON.parse(content); + spec = JSON.parse(json); } catch (e) { - const result: IConfig = { + flaggableSpec = { spec: null, hasFlags: true, reason: `malformed JSON` }; - content = JSON.stringify(result); } if (spec) { - const config = j2c(spec); - content = JSON.stringify(config); + flaggableSpec = flagger(spec); + } + if (flaggableSpec) { + json = JSON.stringify(flaggableSpec); } } - return sanitizedHTML('div', { class: className }, content, true); + return sanitizedHTML('div', { class: className }, json, true); }, - hydrateConfig: (renderer, errorHandler) => { - return hydrateConfig(pluginName, className, renderer, errorHandler); + hydrateSpecs: (renderer, errorHandler) => { + const specContainers: SpecContainer[] = []; + const containers = renderer.element.querySelectorAll(`.${className}`); + for (const [index, container] of Array.from(containers).entries()) { + const flaggableSpec = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); + if (!flaggableSpec) continue; + specContainers.push({ container: container as HTMLElement, flaggableSpec }); + } + return specContainers; }, }; - return basePlugin; + return plugin; } diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index 7588d7d4..38c7510b 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -3,11 +3,11 @@ * Licensed under the MIT License. */ -import { definePlugin, IConfig, IConfigContainer, IInstance, Plugin } from '../factory.js'; +import { definePlugin, FlaggableSpec, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import * as Csstree from 'css-tree'; -import { getJsonScriptTag, pluginClassName } from './util.js'; -import { configPlugin, hydrateConfig } from './config.js'; +import { pluginClassName } from './util.js'; +import { flaggableJsonPlugin } from './config.js'; // CSS Tree is expected to be available as a global variable declare const csstree: typeof Csstree; @@ -88,7 +88,7 @@ function categorizeCss(cssContent: string) { atRules: {}, }; - const result: IConfig = { + const result: FlaggableSpec = { spec, hasFlags: false }; @@ -311,7 +311,7 @@ const pluginName = 'css'; const className = pluginClassName(pluginName); export const cssPlugin: Plugin = { - ...configPlugin(pluginName, className), + ...flaggableJsonPlugin(pluginName, className), initializePlugin: (md) => { // Check for required css-tree dependency if (typeof csstree === 'undefined') { @@ -374,11 +374,11 @@ export const cssPlugin: Plugin = { for (const [index, configContainer] of Array.from(configContainers).entries()) { - const categorizedCss = configContainer.config.spec; + const categorizedCss = configContainer.flaggableSpec.spec; const comments: string[] = []; // Log security issues found - if (configContainer.config.hasFlags) { + if (configContainer.flaggableSpec.hasFlags) { console.warn(`CSS security: Security issues detected in CSS`); comments.push(``); } diff --git a/packages/markdown/src/plugins/vega-lite.ts b/packages/markdown/src/plugins/vega-lite.ts index 30e062bf..26766d4b 100644 --- a/packages/markdown/src/plugins/vega-lite.ts +++ b/packages/markdown/src/plugins/vega-lite.ts @@ -3,47 +3,48 @@ * Licensed under the MIT License. */ -import { Spec } from 'vega'; -import { definePlugin, IConfig, Plugin } from '../factory.js'; +import { FlaggableSpec, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; -import { getJsonScriptTag, pluginClassName } from './util.js'; -import { inspectSpec, vegaPlugin } from './vega.js'; -import { compile } from 'vega-lite'; +import { flaggableJsonPlugin } from './config.js'; +import { pluginClassName } from './util.js'; +import { inspectVegaSpec, vegaPlugin } from './vega.js'; +import { compile, TopLevelSpec } from 'vega-lite'; const pluginName = 'vega-lite'; const className = pluginClassName(pluginName); -export const vegaLitePlugin: Plugin = { - name: pluginName, +export function inspectVegaLiteSpec(spec: TopLevelSpec) { + //TODO inspect spec for flags + const flaggableSpec: FlaggableSpec = { + spec, + }; + return flaggableSpec; +} + +export const vegaLitePlugin: Plugin = { + ...flaggableJsonPlugin(pluginName, className, inspectVegaLiteSpec), hydratesBefore: vegaPlugin.name, - initializePlugin: (md) => definePlugin(md, pluginName), - fence: token => { - return sanitizedHTML('div', { class: className }, token.content.trim(), true); - }, - hydrateComponent: async (renderer, errorHandler) => { - const containers = renderer.element.querySelectorAll(`.${className}`); - for (const [index, container] of Array.from(containers).entries()) { - const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); - if (!jsonObj) continue; + hydrateComponent: async (renderer, errorHandler, configContainers) => { + for (const [index, configContainer] of Array.from(configContainers).entries()) { //compile to vega try { - const { spec } = compile(jsonObj); + const { spec } = compile(configContainer.flaggableSpec.spec); - const result = inspectSpec(spec); + const result = inspectVegaSpec(spec); //create both a script tag and a vega tag for the vega plugin to catch const html = sanitizedHTML('div', { class: pluginClassName(vegaPlugin.name) }, JSON.stringify(result, null, 2), true); //append the html right after this container - container.insertAdjacentHTML('afterend', html); + configContainer.container.insertAdjacentHTML('afterend', html); //the container can be removed - container.remove(); + configContainer.container.remove(); } catch (error) { //did not compile - errorHandler(error, pluginName, index, 'compile', container); + errorHandler(error, pluginName, index, 'compile', configContainer.container); } } diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index d8b89909..dd4326b1 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -4,13 +4,13 @@ */ import { changeset, parse, View, expressionFunction, LoggerInterface } from 'vega'; -import { Batch, IInstance, Plugin, PrioritizedSignal, IConfig } from '../factory.js'; +import { Batch, IInstance, Plugin, PrioritizedSignal, FlaggableSpec } from '../factory.js'; import { BaseSignal, InitSignal, NewSignal, Runtime, Spec, ValuesData } from 'vega-typings'; import { ErrorHandler, Renderer } from '../renderer.js'; import { LogLevel } from '../signalbus.js'; import { pluginClassName, urlParam } from './util.js'; import { defaultCommonOptions } from 'common'; -import { configPlugin } from './config.js'; +import { flaggableJsonPlugin } from './config.js'; const ignoredSignals = ['width', 'height', 'padding', 'autosize', 'background', 'style', 'parent', 'datum', 'item', 'event', 'cursor']; @@ -32,16 +32,16 @@ interface VegaInstance extends SpecInit { const pluginName = 'vega'; const className = pluginClassName(pluginName); -export function inspectSpec(spec: Spec) { +export function inspectVegaSpec(spec: Spec) { //TODO inspect spec for flags - const result: IConfig = { + const flaggableSpec: FlaggableSpec = { spec, }; - return result; + return flaggableSpec; } export const vegaPlugin: Plugin = { - ...configPlugin(pluginName, className, inspectSpec), + ...flaggableJsonPlugin(pluginName, className, inspectVegaSpec), hydrateComponent: async (renderer, errorHandler, configContainers) => { //initialize the expressionFunction only once if (!expressionsInitialized) { @@ -53,7 +53,7 @@ export const vegaPlugin: Plugin = { const specInits: SpecInit[] = []; for (const [index, configContainer] of Array.from(configContainers).entries()) { - const specInit = createSpecInit(configContainer.container, index, configContainer.config.spec); + const specInit = createSpecInit(configContainer.container, index, configContainer.flaggableSpec.spec); if (specInit) { specInits.push(specInit); } diff --git a/packages/markdown/src/renderer.ts b/packages/markdown/src/renderer.ts index 606436aa..7880a6c6 100644 --- a/packages/markdown/src/renderer.ts +++ b/packages/markdown/src/renderer.ts @@ -5,7 +5,7 @@ import MarkdownIt from 'markdown-it'; import { Renderers } from 'vega-typings'; -import { create, IConfigContainer, IInstance, plugins } from './factory.js'; +import { create, SpecContainer, IInstance, plugins } from './factory.js'; import { SignalBus } from './signalbus.js'; import { defaultCommonOptions } from 'common'; @@ -97,9 +97,9 @@ export class Renderer { for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; - let configContainers: IConfigContainer<{}>[]; - if (plugin.hydrateConfig) { - configContainers = plugin.hydrateConfig(this, this.options.errorHandler); + let configContainers: SpecContainer<{}>[]; + if (plugin.hydrateSpecs) { + configContainers = plugin.hydrateSpecs(this, this.options.errorHandler); } if (plugin.hydrateComponent) { //make a new promise that returns IInstances but adds the plugin name From 9f91ca105be4eaddd4ce997db868cf3929fe910f Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 17:08:40 -0700 Subject: [PATCH 03/20] hydrate specs --- packages/common/src/index.ts | 1 + packages/common/src/messages.ts | 5 +- packages/common/src/types.ts | 11 ++++ packages/markdown/src/factory.ts | 12 ++--- packages/markdown/src/plugins/config.ts | 14 +++--- packages/markdown/src/plugins/css.ts | 12 +++-- packages/markdown/src/plugins/vega-lite.ts | 58 ++++++++++++---------- packages/markdown/src/plugins/vega.ts | 13 ++--- packages/markdown/src/renderer.ts | 45 +++++++++++------ 9 files changed, 103 insertions(+), 68 deletions(-) create mode 100644 packages/common/src/types.ts diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 4bb11d8c..75c62e56 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,2 +1,3 @@ export * from './common.js'; export * from './messages.js'; +export * from './types.js'; diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index 3bbe49ed..6da32ac7 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -1,4 +1,5 @@ import { InteractiveDocument } from "schema"; +import { Flagged } from "./types.js"; export interface SandboxRenderMessage { type: 'sandboxRender'; @@ -8,14 +9,14 @@ export interface SandboxRenderMessage { export interface SandboxedPreHydrateMessage { type: 'sandboxedPreHydrate'; transactionId: number; - //todo put stuff here for whitelist + flags: Flagged<{}>[]; } export type SandboxApprovalMessage = { type: 'sandboxApproval'; transactionId: number; approved: boolean; - //todo: add more fields as needed + remediated: Flagged<{}>[]; }; export interface HostRenderRequestMessage { diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts new file mode 100644 index 00000000..3217c862 --- /dev/null +++ b/packages/common/src/types.ts @@ -0,0 +1,11 @@ +export interface FlaggableSpec { + spec: T; + hasFlags?: boolean; + reason?: string; +} + +export interface Flagged { + pluginName: string; + containerId: string; + flaggableSpec: FlaggableSpec; +} diff --git a/packages/markdown/src/factory.ts b/packages/markdown/src/factory.ts index 8411a5dd..0589a03f 100644 --- a/packages/markdown/src/factory.ts +++ b/packages/markdown/src/factory.ts @@ -7,7 +7,7 @@ import MarkdownIt, { Token } from 'markdown-it/index.js'; import { attrs } from '@mdit/plugin-attrs'; import { container, MarkdownItContainerOptions } from '@mdit/plugin-container'; import { ErrorHandler, Renderer } from './renderer.js'; -import { defaultCommonOptions } from 'common'; +import { defaultCommonOptions, FlaggableSpec, Flagged } from 'common'; declare const markdownit: typeof MarkdownIt; @@ -37,12 +37,6 @@ export interface IInstance { getCurrentSignalValue?: (signalName: string) => unknown; } -export interface FlaggableSpec { - spec: T; - hasFlags?: boolean; - reason?: string; -} - export interface SpecContainer { container: HTMLElement; flaggableSpec: FlaggableSpec; @@ -53,8 +47,8 @@ export interface Plugin { hydratesBefore?: string; initializePlugin: (md: MarkdownIt) => void; fence?: (token: Token, idx: number) => string; - hydrateSpecs?: (renderer: Renderer, errorHandler: ErrorHandler) => SpecContainer[]; - hydrateComponent?: (renderer: Renderer, errorHandler: ErrorHandler, specContainers?: SpecContainer[]) => Promise; + hydrateSpecs?: (renderer: Renderer, errorHandler: ErrorHandler) => Flagged[]; + hydrateComponent?: (renderer: Renderer, errorHandler: ErrorHandler, flagged: Flagged[]) => Promise; } export const plugins: Plugin[] = []; diff --git a/packages/markdown/src/plugins/config.ts b/packages/markdown/src/plugins/config.ts index b55fbae8..b1e5e3f1 100644 --- a/packages/markdown/src/plugins/config.ts +++ b/packages/markdown/src/plugins/config.ts @@ -1,12 +1,13 @@ -import { definePlugin, FlaggableSpec, SpecContainer, Plugin } from "../factory.js"; +import { definePlugin, Plugin } from "../factory.js"; import { sanitizedHTML } from "../sanitize.js"; import { getJsonScriptTag } from "./util.js"; +import { FlaggableSpec, Flagged } from 'common'; export function flaggableJsonPlugin(pluginName: string, className: string, flagger?: (spec: T) => FlaggableSpec) { const plugin: Plugin = { name: pluginName, initializePlugin: (md) => definePlugin(md, pluginName), - fence: token => { + fence: (token, index) => { let json = token.content.trim(); if (flagger) { let spec: T; @@ -27,17 +28,18 @@ export function flaggableJsonPlugin(pluginName: string, className: string, fl json = JSON.stringify(flaggableSpec); } } - return sanitizedHTML('div', { class: className }, json, true); + return sanitizedHTML('div', { class: className, id: `${pluginName}-${index}` }, json, true); }, hydrateSpecs: (renderer, errorHandler) => { - const specContainers: SpecContainer[] = []; + const flagged: Flagged[] = []; const containers = renderer.element.querySelectorAll(`.${className}`); for (const [index, container] of Array.from(containers).entries()) { + const id = container.id; const flaggableSpec = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); if (!flaggableSpec) continue; - specContainers.push({ container: container as HTMLElement, flaggableSpec }); + flagged.push({ pluginName, containerId: container.id, flaggableSpec }); } - return specContainers; + return flagged; }, }; return plugin; diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index 38c7510b..e6c9792e 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. */ -import { definePlugin, FlaggableSpec, IInstance, Plugin } from '../factory.js'; +import { definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import * as Csstree from 'css-tree'; import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; +import { FlaggableSpec } from 'common'; // CSS Tree is expected to be available as a global variable declare const csstree: typeof Csstree; @@ -358,7 +359,7 @@ export const cssPlugin: Plugin = { // Parse and categorize CSS content const categorizedCss = categorizeCss(cssContent); - return sanitizedHTML('div', { class: className }, JSON.stringify(categorizedCss), true); + return sanitizedHTML('div', { id: `${pluginName}-${idx}`, class: className }, JSON.stringify(categorizedCss), true); } // Fallback to original fence renderer @@ -372,7 +373,9 @@ export const cssPlugin: Plugin = { hydrateComponent: async (renderer, errorHandler, configContainers) => { const cssInstances: { id: string; element: HTMLStyleElement }[] = []; - for (const [index, configContainer] of Array.from(configContainers).entries()) { + for (let index = 0; index < configContainers.length; index++) { + const configContainer = configContainers[index]; + const container = renderer.element.querySelector(`#${configContainer.containerId}`); const categorizedCss = configContainer.flaggableSpec.spec; const comments: string[] = []; @@ -405,7 +408,8 @@ export const cssPlugin: Plugin = { } else { comments.push(``); } - configContainer.container.innerHTML = comments.join('\n'); + + container.innerHTML = comments.join('\n'); } const instances: IInstance[] = cssInstances.map((cssInstance) => { diff --git a/packages/markdown/src/plugins/vega-lite.ts b/packages/markdown/src/plugins/vega-lite.ts index 26766d4b..778a2509 100644 --- a/packages/markdown/src/plugins/vega-lite.ts +++ b/packages/markdown/src/plugins/vega-lite.ts @@ -3,12 +3,14 @@ * Licensed under the MIT License. */ -import { FlaggableSpec, Plugin } from '../factory.js'; +import { FlaggableSpec } from 'common'; +import { Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { flaggableJsonPlugin } from './config.js'; import { pluginClassName } from './util.js'; import { inspectVegaSpec, vegaPlugin } from './vega.js'; import { compile, TopLevelSpec } from 'vega-lite'; +import { Spec } from 'vega'; const pluginName = 'vega-lite'; const className = pluginClassName(pluginName); @@ -22,33 +24,37 @@ export function inspectVegaLiteSpec(spec: TopLevelSpec) { } export const vegaLitePlugin: Plugin = { - ...flaggableJsonPlugin(pluginName, className, inspectVegaLiteSpec), - hydratesBefore: vegaPlugin.name, - hydrateComponent: async (renderer, errorHandler, configContainers) => { - for (const [index, configContainer] of Array.from(configContainers).entries()) { - - //compile to vega + ...flaggableJsonPlugin(pluginName, className), + fence: (token, index) => { + let json = token.content.trim(); + let spec: TopLevelSpec; + let flaggableSpec: FlaggableSpec; + try { + spec = JSON.parse(json); + } catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reason: `malformed JSON` + }; + } + if (spec) { try { - const { spec } = compile(configContainer.flaggableSpec.spec); - - const result = inspectVegaSpec(spec); - - //create both a script tag and a vega tag for the vega plugin to catch - const html = sanitizedHTML('div', { class: pluginClassName(vegaPlugin.name) }, JSON.stringify(result, null, 2), true); - - //append the html right after this container - configContainer.container.insertAdjacentHTML('afterend', html); - - //the container can be removed - configContainer.container.remove(); - - } catch (error) { - //did not compile - errorHandler(error, pluginName, index, 'compile', configContainer.container); + const vegaSpec = compile(spec); + flaggableSpec = inspectVegaSpec(vegaSpec.spec); + } + catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reason: `failed to compile vega spec` + }; } } - - //return a promise that resolves to an empty array of IInstance - return Promise.resolve([]); + if (flaggableSpec) { + json = JSON.stringify(flaggableSpec); + } + return sanitizedHTML('div', { class: pluginClassName(vegaPlugin.name), id: `${pluginName}-${index}` }, json, true); }, + hydratesBefore: vegaPlugin.name, }; diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index dd4326b1..01de34a7 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -4,13 +4,13 @@ */ import { changeset, parse, View, expressionFunction, LoggerInterface } from 'vega'; -import { Batch, IInstance, Plugin, PrioritizedSignal, FlaggableSpec } from '../factory.js'; +import { Batch, IInstance, Plugin, PrioritizedSignal } from '../factory.js'; import { BaseSignal, InitSignal, NewSignal, Runtime, Spec, ValuesData } from 'vega-typings'; import { ErrorHandler, Renderer } from '../renderer.js'; import { LogLevel } from '../signalbus.js'; import { pluginClassName, urlParam } from './util.js'; -import { defaultCommonOptions } from 'common'; -import { flaggableJsonPlugin } from './config.js'; +import { defaultCommonOptions, FlaggableSpec } from 'common'; +import { flaggableJsonPlugin, } from './config.js'; const ignoredSignals = ['width', 'height', 'padding', 'autosize', 'background', 'style', 'parent', 'datum', 'item', 'event', 'cursor']; @@ -51,9 +51,10 @@ export const vegaPlugin: Plugin = { const vegaInstances: VegaInstance[] = []; const specInits: SpecInit[] = []; - for (const [index, configContainer] of Array.from(configContainers).entries()) { - - const specInit = createSpecInit(configContainer.container, index, configContainer.flaggableSpec.spec); + for (let index = 0; index < configContainers.length; index++) { + const configContainer = configContainers[index]; + const container = renderer.element.querySelector(`#${configContainer.containerId}`); + const specInit = createSpecInit(container, index, configContainer.flaggableSpec.spec); if (specInit) { specInits.push(specInit); } diff --git a/packages/markdown/src/renderer.ts b/packages/markdown/src/renderer.ts index 7880a6c6..171a30c1 100644 --- a/packages/markdown/src/renderer.ts +++ b/packages/markdown/src/renderer.ts @@ -5,9 +5,9 @@ import MarkdownIt from 'markdown-it'; import { Renderers } from 'vega-typings'; -import { create, SpecContainer, IInstance, plugins } from './factory.js'; +import { create, IInstance, plugins } from './factory.js'; import { SignalBus } from './signalbus.js'; -import { defaultCommonOptions } from 'common'; +import { defaultCommonOptions, Flagged } from 'common'; export interface ErrorHandler { (error: Error, pluginName: string, instanceIndex: number, phase: string, container: Element, detail?: string): void; @@ -64,14 +64,13 @@ export class Renderer { async render(markdown: string) { //loop through all the destroy handlers and call them. have the key there to help us debug - await this.reset(); + this.reset(); - const content = this.renderHtml(markdown); + const html = this.renderHtml(markdown); + this.element.innerHTML = html; + const hydratedSpecs = this.hydrateSpecs(); - // Set all content at once - this.element.innerHTML = content; - - await this.hydrate(); + await this.hydrate(hydratedSpecs); } renderHtml(markdown: string) { @@ -88,22 +87,38 @@ export class Renderer { return content; } - async hydrate() { + hydrateSpecs() { this.ensureMd(); - //loop through all the plugins and render them - this.signalBus.log('Renderer', 'rendering DOM'); - const hydrationPromises: Promise[] = []; + const allFlagged: Flagged<{}>[] = []; + + //loop through all the plugins and hydrate their specs and flag them id needed + this.signalBus.log('Renderer', 'hydrate specs'); for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; - let configContainers: SpecContainer<{}>[]; if (plugin.hydrateSpecs) { - configContainers = plugin.hydrateSpecs(this, this.options.errorHandler); + allFlagged.push(...plugin.hydrateSpecs(this, this.options.errorHandler)); } + } + + return allFlagged; + } + + async hydrate(hydratedSpecs: Flagged<{}>[]) { + this.ensureMd(); + + //loop through all the plugins and render them + this.signalBus.log('Renderer', 'hydrate components'); + const hydrationPromises: Promise[] = []; + + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; if (plugin.hydrateComponent) { + //get only those specs that match the plugin name + const specContainers = hydratedSpecs.filter(spec => spec.pluginName === plugin.name); //make a new promise that returns IInstances but adds the plugin name - hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler, configContainers).then(instances => { + hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler, specContainers).then(instances => { return { pluginName: plugin.name, instances, From d1d3b0d31d0f75c0f799371d1e6d47db9c30d5b7 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 20:12:30 -0700 Subject: [PATCH 04/20] onApprove handler --- packages/editor/dev/previewer.ts | 50 +++------------------ packages/editor/src/sandbox.tsx | 34 ++++++-------- packages/host/src/listener.ts | 27 ++++++++--- packages/host/src/post-receive.ts | 17 +------ packages/sandbox-resources/src/sandboxed.ts | 28 +++++++----- packages/sandbox/index.html | 34 +++++++------- packages/sandbox/src/preview.ts | 3 +- packages/sandbox/src/sandbox.ts | 17 ++++++- packages/vscode-resources/src/html-json.ts | 46 +++++++------------ 9 files changed, 110 insertions(+), 146 deletions(-) diff --git a/packages/editor/dev/previewer.ts b/packages/editor/dev/previewer.ts index cb2c43a5..e6c187b0 100644 --- a/packages/editor/dev/previewer.ts +++ b/packages/editor/dev/previewer.ts @@ -1,12 +1,8 @@ import { Renderer } from '@microsoft/interactive-document-markdown'; import { Previewer, PreviewerOptions } from '@microsoft/chartifact-sandbox'; -import { SandboxApprovalMessage } from 'common'; - -let transactionIndex = 0; export class DevPreviewer extends Previewer { public renderer: Renderer; - public transactions: Record = {}; constructor(elementOrSelector: string | HTMLElement, markdown: string, options: PreviewerOptions) { super(elementOrSelector, markdown, options); @@ -34,22 +30,12 @@ export class DevPreviewer extends Previewer { render(markdown: string) { const html = this.renderer.renderHtml(markdown); - - const doc = new DOMParser().parseFromString(html, 'text/html'); - const transactionId = transactionIndex++; - this.transactions[transactionId] = doc; - - //inline approval - const sandboxedPreRenderMessage: SandboxApprovalMessage = { - type: 'sandboxApproval', - transactionId, - approved: true, - // Additional data for whitelist can be added here - }; - - //inline approval - this.approve(sandboxedPreRenderMessage); - + const newHtml = ` + +${html}`; + this.renderer.element.innerHTML = newHtml; + const x = this.renderer.hydrateSpecs(); + this.renderer.hydrate(x); } send(markdown: string) { @@ -61,30 +47,6 @@ export class DevPreviewer extends Previewer { } } - approve(message: SandboxApprovalMessage): void { - if (message.approved) { - //only handle if the transactionId is the latest - if (message.transactionId === transactionIndex - 1) { - - console.log('Sandbox approval received:', message.transactionId, transactionIndex); - if (message.transactionId === transactionIndex - 1) { - const doc = this.transactions[message.transactionId]; - if (doc) { - const newHtml = ` - -${doc.body.innerHTML}`; - this.renderer.element.innerHTML = newHtml; - this.renderer.hydrate(); - } - } - } else { - console.warn('Received sandbox approval for an outdated transaction:', message.transactionId, transactionIndex); - } - } else { - console.warn('Sandbox approval denied:', message.transactionId); - } - } - private displayError(error: unknown) { this.element.innerHTML = `
Error: ${error instanceof Error ? error.message : String(error)} diff --git a/packages/editor/src/sandbox.tsx b/packages/editor/src/sandbox.tsx index 5c793d59..a739dd7b 100644 --- a/packages/editor/src/sandbox.tsx +++ b/packages/editor/src/sandbox.tsx @@ -14,7 +14,6 @@ export class SandboxDocumentPreview extends React.Component void; constructor(props: SandboxDocumentPreviewProps) { super(props); @@ -37,24 +36,6 @@ export class SandboxDocumentPreview extends React.Component { this.isSandboxReady = true; - this.windowMessageReceivedHandler = (event: MessageEvent) => { - const message = event.data as SandboxedPreHydrateMessage; - if (message.type === 'sandboxedPreHydrate' && this.sandboxRef) { - // Handle sandboxed pre-render message - console.log('Handling sandboxed pre-render message:', message); - - // Approve the sandboxed pre-render - const sandboxedApprovalMessage: SandboxApprovalMessage = { - type: 'sandboxApproval', - transactionId: message.transactionId, - approved: true, - - }; - this.sandboxRef.approve(sandboxedApprovalMessage); - } - }; - window.addEventListener('message', this.windowMessageReceivedHandler); - // Process pending update if (this.pendingUpdate) { this.processUpdate(this.pendingUpdate); @@ -62,6 +43,20 @@ export class SandboxDocumentPreview extends React.Component console.error('Sandbox initialization failed:', error), + onApprove: (message: SandboxedPreHydrateMessage) => { + // Handle sandboxed pre-render message + console.log('Handling sandboxed pre-render message:', message); + const remediated = message.flags; + + // Approve the sandboxed pre-render + const sandboxedApprovalMessage: SandboxApprovalMessage = { + type: 'sandboxApproval', + transactionId: message.transactionId, + approved: true, + remediated, + }; + return sandboxedApprovalMessage; + }, } ); } catch (error) { @@ -99,7 +94,6 @@ export class SandboxDocumentPreview extends React.Component(elementOrSelector: string | T): T | null { if (typeof elementOrSelector === 'string') { @@ -114,30 +115,44 @@ export class Listener { }, onError: () => { this.errorHandler(new Error('Sandbox initialization failed'), 'Sandbox could not be initialized'); - } + }, + onApprove: (message) => { + // Handle sandboxed pre-render message + console.log('Handling sandboxed pre-render message:', message); + const remediated = message.flags; + + // Approve the sandboxed pre-render + const sandboxedApprovalMessage: SandboxApprovalMessage = { + type: 'sandboxApproval', + transactionId: message.transactionId, + approved: true, + remediated, + }; + return sandboxedApprovalMessage; + }, }); } public errorHandler(error: Error, detailsHtml: string) { show(this.loadingDiv, false); - + // Create DOM elements safely to prevent XSS const errorDiv = document.createElement('div'); errorDiv.style.color = 'red'; errorDiv.style.padding = '20px'; - + const errorLabel = document.createElement('strong'); errorLabel.textContent = 'Error:'; - + const errorMessage = document.createTextNode(` ${error.message}`); const lineBreak = document.createElement('br'); const details = document.createTextNode(detailsHtml); - + errorDiv.appendChild(errorLabel); errorDiv.appendChild(errorMessage); errorDiv.appendChild(lineBreak); errorDiv.appendChild(details); - + // Clear previous content and append the error safely this.appDiv.innerHTML = ''; this.appDiv.appendChild(errorDiv); diff --git a/packages/host/src/post-receive.ts b/packages/host/src/post-receive.ts index ce139910..33916f2e 100644 --- a/packages/host/src/post-receive.ts +++ b/packages/host/src/post-receive.ts @@ -13,7 +13,7 @@ export function setupPostMessageHandling(host: Listener) { return; } - const message = event.data as HostRenderRequestMessage | SandboxedPreHydrateMessage; + const message = event.data as HostRenderRequestMessage; if (message.type == 'hostRenderRequest') { if (message.markdown) { host.render(message.markdown, undefined); @@ -22,21 +22,6 @@ export function setupPostMessageHandling(host: Listener) { } else { //do nothing, as messages may be directed to the page for other purposes } - } else if (message.type == 'sandboxedPreHydrate') { - //make sure its from the sandbox iframe - if (event.source === host.sandbox.iframe.contentWindow) { - // TODO check whitelist and do a mutation if needed - console.log('TODO: handle sandboxed pre-render message:', message); - - //approve the sandboxed pre-render - const sandboxedApprovalMessage: SandboxApprovalMessage = { - type: 'sandboxApproval', - transactionId: message.transactionId, - approved: true, - //todo: mutations - }; - host.sandbox.approve(sandboxedApprovalMessage); - } } } catch (error) { host.errorHandler( diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index 2dd5e296..a36830a9 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -1,11 +1,13 @@ declare const renderRequest: IDocs.common.SandboxRenderMessage; +let renderer: IDocs.markdown.Renderer; + document.addEventListener('DOMContentLoaded', () => { let transactionIndex = 0; - const transactions: Record = {}; + const transactions: Record[]> = {}; - const renderer = new IDocs.markdown.Renderer(document.body, { + renderer = new IDocs.markdown.Renderer(document.body, { errorHandler: (error: Error, pluginName: string, instanceIndex: number, phase: string, container: Element, detail?: string) => { console.error(`Error in plugin ${pluginName} at instance ${instanceIndex} during ${phase}:`, error); if (detail) { @@ -18,21 +20,22 @@ document.addEventListener('DOMContentLoaded', () => { function render(request: IDocs.common.SandboxRenderMessage) { if (request.markdown) { renderer.reset(); - const html = renderer.renderHtml(request.markdown); - //look at dom elements prior to hydration - const parser = new DOMParser(); - const doc = parser.parseFromString(html, 'text/html'); + //debugger; + + const html = renderer.renderHtml(request.markdown); + renderer.element.innerHTML = html; + const flags = renderer.hydrateSpecs(); //todo: get stuff here for whitelist const transactionId = transactionIndex++; - transactions[transactionId] = doc; + transactions[transactionId] = flags; //send message to parent to ask for whitelist const sandboxedPreRenderMessage: IDocs.common.SandboxedPreHydrateMessage = { type: 'sandboxedPreHydrate', transactionId, - //todo put stuff here for whitelist + flags, }; window.parent.postMessage(sandboxedPreRenderMessage, '*'); } @@ -56,16 +59,17 @@ document.addEventListener('DOMContentLoaded', () => { console.log('Sandbox approval received:', message.transactionId, transactionIndex); + //debugger; + //only handle if the transactionId is the latest if (message.transactionId === transactionIndex - 1) { //todo: mutate the document according to approval //hydrate the renderer - const doc = transactions[message.transactionId]; - if (doc) { - renderer.element.innerHTML = doc.body.innerHTML; - renderer.hydrate(); + const flags = transactions[message.transactionId]; + if (flags) { + renderer.hydrate(flags); } } else { console.warn('Received sandbox approval for an outdated transaction:', message.transactionId, transactionIndex); diff --git a/packages/sandbox/index.html b/packages/sandbox/index.html index cd78da82..3a26ff38 100644 --- a/packages/sandbox/index.html +++ b/packages/sandbox/index.html @@ -38,27 +38,27 @@ diff --git a/packages/sandbox/src/preview.ts b/packages/sandbox/src/preview.ts index f86933b8..3bea0489 100644 --- a/packages/sandbox/src/preview.ts +++ b/packages/sandbox/src/preview.ts @@ -1,8 +1,9 @@ -import { SandboxApprovalMessage } from "common/dist/esnext/messages.js"; +import { SandboxApprovalMessage, SandboxedPreHydrateMessage } from "common/dist/esnext/messages.js"; export interface PreviewerOptions { onReady?: () => void; onError?: (error: Error) => void; + onApprove?: (message: SandboxedPreHydrateMessage) => SandboxApprovalMessage; } // Previewer class diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index 92faa4d5..7673a264 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -2,7 +2,7 @@ import { Previewer, PreviewerOptions } from './preview.js'; import { rendererHtml } from './resources/rendererHtml.js'; import { rendererUmdJs } from './resources/rendererUmdJs.js'; import { sandboxedJs } from './resources/sandboxedJs.js'; -import type { SandboxRenderMessage, SandboxApprovalMessage } from 'common'; +import type { SandboxRenderMessage, SandboxApprovalMessage, SandboxedPreHydrateMessage } from 'common'; export class Sandbox extends Previewer { public iframe: HTMLIFrameElement; @@ -27,6 +27,21 @@ export class Sandbox extends Previewer { console.error('Error loading iframe:', error); options?.onError?.(new Error('Failed to load iframe')); }); + + window.addEventListener('message', (event) => { + if (event.source === this.iframe.contentWindow) { + const message = event.data as SandboxedPreHydrateMessage; + if (message.type == 'sandboxedPreHydrate') { + //make sure its from the sandbox iframe + if (this.options?.onApprove) { + // TODO check whitelist and do a mutation if needed + // TODO loop through all flagged + const sandboxedApprovalMessage = this.options.onApprove(message); + this.approve(sandboxedApprovalMessage); + } + } + } + }); } destroy() { diff --git a/packages/vscode-resources/src/html-json.ts b/packages/vscode-resources/src/html-json.ts index 0a163cb8..8ff103e1 100644 --- a/packages/vscode-resources/src/html-json.ts +++ b/packages/vscode-resources/src/html-json.ts @@ -15,37 +15,25 @@ window.addEventListener('DOMContentLoaded', () => { markdown = 'Failed to parse Interactive Document JSON'; } if (!sandbox) { - sandbox = new IDocs.sandbox.Sandbox('main', markdown); + sandbox = new IDocs.sandbox.Sandbox('main', markdown, { + onApprove: (message) => { + // Handle sandboxed pre-render message + console.log('Handling sandboxed pre-render message:', message); + const remediated = message.flags; - window.addEventListener('message', (event) => { - try { - // Validate the message structure - if (!event.data || typeof event.data !== 'object') { - return; - } - - const message = event.data as IDocs.common.SandboxedPreHydrateMessage; - if (message.type == 'sandboxedPreHydrate') { - //make sure its from the sandbox iframe - if (event.source === sandbox.iframe.contentWindow) { - // TODO check whitelist and do a mutation if needed - console.log('TODO: handle sandboxed pre-render message:', message); - - //approve the sandboxed pre-render - const sandboxedApprovalMessage: IDocs.common.SandboxApprovalMessage = { - type: 'sandboxApproval', - transactionId: message.transactionId, - approved: true, - //todo: mutations - }; - sandbox.approve(sandboxedApprovalMessage); - } - } - } catch (error) { - console.error('Error processing postMessage event:', error); - } + // Approve the sandboxed pre-render + const sandboxedApprovalMessage: IDocs.common.SandboxApprovalMessage = { + type: 'sandboxApproval', + transactionId: message.transactionId, + approved: true, + remediated, + }; + return sandboxedApprovalMessage; + }, + onError: (error) => { + console.error('Sandbox error:', error); + }, }); - } else { sandbox.send(markdown); } From 8898fbcd5b4ec1443c6d5ec4b2b0a29871c0dbc6 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 20:16:42 -0700 Subject: [PATCH 05/20] move to dev typescript --- packages/sandbox/dev/index.ts | 26 ++++++++++++++++++++++++++ packages/sandbox/index.html | 26 +------------------------- 2 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 packages/sandbox/dev/index.ts diff --git a/packages/sandbox/dev/index.ts b/packages/sandbox/dev/index.ts new file mode 100644 index 00000000..f0a7e0c7 --- /dev/null +++ b/packages/sandbox/dev/index.ts @@ -0,0 +1,26 @@ +import { Sandbox } from '../src/index.ts'; +const textarea = document.getElementById('md') as HTMLTextAreaElement; +const sandbox = new Sandbox(document.body, textarea.value, { + onReady: () => { + console.log('Sandbox is ready'); + }, + onError: (error) => { + console.error('Sandbox error:', error); + }, + onApprove: (message) => { + console.log('Sandbox approval message:', message); + return { + type: 'sandboxApproval', + transactionId: message.transactionId, + approved: true, + remediated: message.flags, + }; + } +}); + +//allow sandbox to be accessed globally for debugging +window['sandbox'] = sandbox; + +textarea.addEventListener('input', () => { + sandbox.send(textarea.value); +}); diff --git a/packages/sandbox/index.html b/packages/sandbox/index.html index 3a26ff38..b7bc9f91 100644 --- a/packages/sandbox/index.html +++ b/packages/sandbox/index.html @@ -35,31 +35,7 @@ ``` - + From 25cfdc28c8aadb21fc90979a859900e8d9ba0add Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 20:17:17 -0700 Subject: [PATCH 06/20] use approve handler --- .../vscode-resources/src/html-markdown.ts | 45 +++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/packages/vscode-resources/src/html-markdown.ts b/packages/vscode-resources/src/html-markdown.ts index a4b6fddd..6829b1d2 100644 --- a/packages/vscode-resources/src/html-markdown.ts +++ b/packages/vscode-resources/src/html-markdown.ts @@ -1,34 +1,23 @@ window.addEventListener('DOMContentLoaded', () => { const textarea = document.getElementById('markdown-input') as HTMLTextAreaElement; - const sandbox = new IDocs.sandbox.Sandbox('main', textarea.value); + const sandbox = new IDocs.sandbox.Sandbox('main', textarea.value, { + onApprove: (message) => { + // Handle sandboxed pre-render message + console.log('Handling sandboxed pre-render message:', message); + const remediated = message.flags; - window.addEventListener('message', (event) => { - try { - // Validate the message structure - if (!event.data || typeof event.data !== 'object') { - return; - } - - const message = event.data as IDocs.common.SandboxedPreHydrateMessage; - if (message.type == 'sandboxedPreHydrate') { - //make sure its from the sandbox iframe - if (event.source === sandbox.iframe.contentWindow) { - // TODO check whitelist and do a mutation if needed - console.log('TODO: handle sandboxed pre-render message:', message); - - //approve the sandboxed pre-render - const sandboxedApprovalMessage: IDocs.common.SandboxApprovalMessage = { - type: 'sandboxApproval', - transactionId: message.transactionId, - approved: true, - //todo: mutations - }; - sandbox.approve(sandboxedApprovalMessage); - } - } - } catch (error) { - console.error('Error processing postMessage event:', error); - } + // Approve the sandboxed pre-render + const sandboxedApprovalMessage: IDocs.common.SandboxApprovalMessage = { + type: 'sandboxApproval', + transactionId: message.transactionId, + approved: true, + remediated, + }; + return sandboxedApprovalMessage; + }, + onError: (error) => { + console.error('Sandbox error:', error); + }, }); textarea.addEventListener('input', () => { From 45a70c0270315523058270729afdbd1f7a8ba72c Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 20:17:30 -0700 Subject: [PATCH 07/20] add logging for peer registration in SignalBus --- packages/markdown/src/signalbus.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/markdown/src/signalbus.ts b/packages/markdown/src/signalbus.ts index a0571121..8579c8eb 100644 --- a/packages/markdown/src/signalbus.ts +++ b/packages/markdown/src/signalbus.ts @@ -88,6 +88,7 @@ export class SignalBus { } registerPeer(peer: IInstance) { + this.log('registerPeer', 'register', peer); this.peers.push(peer); for (const initialSignal of peer.initialSignals) { if (!(initialSignal.name in this.signalDeps)) { From 8de67e84ea24d8f6c1ffa3446001a4c0d1516f51 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 20:57:22 -0700 Subject: [PATCH 08/20] remove approve method --- packages/sandbox/src/preview.ts | 4 ---- packages/sandbox/src/sandbox.ts | 6 +----- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/sandbox/src/preview.ts b/packages/sandbox/src/preview.ts index 3bea0489..cfdb552c 100644 --- a/packages/sandbox/src/preview.ts +++ b/packages/sandbox/src/preview.ts @@ -26,8 +26,4 @@ export class Previewer { send(markdown: string): void { throw new Error('Method not implemented.'); } - - approve(message: SandboxApprovalMessage) { - throw new Error('Method not implemented.'); - } } diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index 7673a264..7a96cf30 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -37,7 +37,7 @@ export class Sandbox extends Previewer { // TODO check whitelist and do a mutation if needed // TODO loop through all flagged const sandboxedApprovalMessage = this.options.onApprove(message); - this.approve(sandboxedApprovalMessage); + this.iframe.contentWindow?.postMessage(sandboxedApprovalMessage, '*'); } } } @@ -59,10 +59,6 @@ export class Sandbox extends Previewer { this.iframe.contentWindow?.postMessage(message, '*'); } - approve(message: SandboxApprovalMessage) { - this.iframe.contentWindow?.postMessage(message, '*'); - } - getDependencies() { return ` From 423789d3101257f86c82a0e6a6ee6d8e0605fe61 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 21:26:51 -0700 Subject: [PATCH 09/20] flatten approval interface --- packages/common/src/messages.ts | 1 - packages/common/src/types.ts | 10 +++----- packages/editor/src/sandbox.tsx | 1 - packages/host/src/listener.ts | 1 - packages/markdown/src/factory.ts | 8 ++++++- packages/markdown/src/plugins/config.ts | 6 ++--- packages/markdown/src/plugins/css.ts | 7 +++--- packages/markdown/src/plugins/vega-lite.ts | 5 ++-- packages/markdown/src/plugins/vega.ts | 8 +++---- packages/sandbox-resources/src/sandboxed.ts | 24 ++++++++----------- packages/sandbox/dev/index.ts | 5 ++-- packages/vscode-resources/src/html-json.ts | 1 - .../vscode-resources/src/html-markdown.ts | 1 - 13 files changed, 35 insertions(+), 43 deletions(-) diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index 6da32ac7..0012799e 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -15,7 +15,6 @@ export interface SandboxedPreHydrateMessage { export type SandboxApprovalMessage = { type: 'sandboxApproval'; transactionId: number; - approved: boolean; remediated: Flagged<{}>[]; }; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 3217c862..8ff1a47d 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -1,11 +1,7 @@ -export interface FlaggableSpec { - spec: T; - hasFlags?: boolean; - reason?: string; -} - export interface Flagged { pluginName: string; containerId: string; - flaggableSpec: FlaggableSpec; + spec: T; + hasFlags?: boolean; + reason?: string; } diff --git a/packages/editor/src/sandbox.tsx b/packages/editor/src/sandbox.tsx index a739dd7b..11ebf5c1 100644 --- a/packages/editor/src/sandbox.tsx +++ b/packages/editor/src/sandbox.tsx @@ -52,7 +52,6 @@ export class SandboxDocumentPreview extends React.Component unknown; } +export interface FlaggableSpec { + spec: T; + hasFlags?: boolean; + reason?: string; +} + export interface SpecContainer { container: HTMLElement; flaggableSpec: FlaggableSpec; diff --git a/packages/markdown/src/plugins/config.ts b/packages/markdown/src/plugins/config.ts index b1e5e3f1..aeb9e42b 100644 --- a/packages/markdown/src/plugins/config.ts +++ b/packages/markdown/src/plugins/config.ts @@ -1,7 +1,7 @@ -import { definePlugin, Plugin } from "../factory.js"; +import { definePlugin, Plugin, FlaggableSpec } from "../factory.js"; import { sanitizedHTML } from "../sanitize.js"; import { getJsonScriptTag } from "./util.js"; -import { FlaggableSpec, Flagged } from 'common'; +import { Flagged } from 'common'; export function flaggableJsonPlugin(pluginName: string, className: string, flagger?: (spec: T) => FlaggableSpec) { const plugin: Plugin = { @@ -37,7 +37,7 @@ export function flaggableJsonPlugin(pluginName: string, className: string, fl const id = container.id; const flaggableSpec = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); if (!flaggableSpec) continue; - flagged.push({ pluginName, containerId: container.id, flaggableSpec }); + flagged.push({ ...flaggableSpec, pluginName, containerId: container.id }); } return flagged; }, diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index e6c9792e..dbc29578 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. */ -import { definePlugin, IInstance, Plugin } from '../factory.js'; +import { definePlugin, IInstance, Plugin, FlaggableSpec } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import * as Csstree from 'css-tree'; import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; -import { FlaggableSpec } from 'common'; // CSS Tree is expected to be available as a global variable declare const csstree: typeof Csstree; @@ -377,11 +376,11 @@ export const cssPlugin: Plugin = { const configContainer = configContainers[index]; const container = renderer.element.querySelector(`#${configContainer.containerId}`); - const categorizedCss = configContainer.flaggableSpec.spec; + const categorizedCss = configContainer.spec; const comments: string[] = []; // Log security issues found - if (configContainer.flaggableSpec.hasFlags) { + if (configContainer.hasFlags) { console.warn(`CSS security: Security issues detected in CSS`); comments.push(``); } diff --git a/packages/markdown/src/plugins/vega-lite.ts b/packages/markdown/src/plugins/vega-lite.ts index 778a2509..e1cdf92b 100644 --- a/packages/markdown/src/plugins/vega-lite.ts +++ b/packages/markdown/src/plugins/vega-lite.ts @@ -3,8 +3,7 @@ * Licensed under the MIT License. */ -import { FlaggableSpec } from 'common'; -import { Plugin } from '../factory.js'; +import { Plugin, FlaggableSpec } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { flaggableJsonPlugin } from './config.js'; import { pluginClassName } from './util.js'; @@ -16,7 +15,7 @@ const pluginName = 'vega-lite'; const className = pluginClassName(pluginName); export function inspectVegaLiteSpec(spec: TopLevelSpec) { - //TODO inspect spec for flags + //TODO inspect spec for flags, such as http:// instead of https://, or other security issues const flaggableSpec: FlaggableSpec = { spec, }; diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index 01de34a7..772836f5 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -4,12 +4,12 @@ */ import { changeset, parse, View, expressionFunction, LoggerInterface } from 'vega'; -import { Batch, IInstance, Plugin, PrioritizedSignal } from '../factory.js'; +import { Batch, IInstance, Plugin, PrioritizedSignal, FlaggableSpec } from '../factory.js'; import { BaseSignal, InitSignal, NewSignal, Runtime, Spec, ValuesData } from 'vega-typings'; import { ErrorHandler, Renderer } from '../renderer.js'; import { LogLevel } from '../signalbus.js'; import { pluginClassName, urlParam } from './util.js'; -import { defaultCommonOptions, FlaggableSpec } from 'common'; +import { defaultCommonOptions } from 'common'; import { flaggableJsonPlugin, } from './config.js'; const ignoredSignals = ['width', 'height', 'padding', 'autosize', 'background', 'style', 'parent', 'datum', 'item', 'event', 'cursor']; @@ -33,7 +33,7 @@ const pluginName = 'vega'; const className = pluginClassName(pluginName); export function inspectVegaSpec(spec: Spec) { - //TODO inspect spec for flags + //TODO inspect spec for flags, such as http:// instead of https://, or other security issues const flaggableSpec: FlaggableSpec = { spec, }; @@ -54,7 +54,7 @@ export const vegaPlugin: Plugin = { for (let index = 0; index < configContainers.length; index++) { const configContainer = configContainers[index]; const container = renderer.element.querySelector(`#${configContainer.containerId}`); - const specInit = createSpecInit(container, index, configContainer.flaggableSpec.spec); + const specInit = createSpecInit(container, index, configContainer.spec); if (specInit) { specInits.push(specInit); } diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index a36830a9..c4242605 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -55,25 +55,21 @@ document.addEventListener('DOMContentLoaded', () => { break; } case 'sandboxApproval': { - if (message.approved) { - console.log('Sandbox approval received:', message.transactionId, transactionIndex); + //debugger; - //debugger; + //only handle if the transactionId is the latest + if (message.transactionId === transactionIndex - 1) { - //only handle if the transactionId is the latest - if (message.transactionId === transactionIndex - 1) { + //todo: mutate the document according to approval - //todo: mutate the document according to approval - - //hydrate the renderer - const flags = transactions[message.transactionId]; - if (flags) { - renderer.hydrate(flags); - } - } else { - console.warn('Received sandbox approval for an outdated transaction:', message.transactionId, transactionIndex); + //hydrate the renderer + const flags = transactions[message.transactionId]; + if (flags) { + renderer.hydrate(flags); } + } else { + console.warn('Received sandbox approval for an outdated transaction:', message.transactionId, transactionIndex); } break; } diff --git a/packages/sandbox/dev/index.ts b/packages/sandbox/dev/index.ts index f0a7e0c7..a4d3b09d 100644 --- a/packages/sandbox/dev/index.ts +++ b/packages/sandbox/dev/index.ts @@ -1,3 +1,4 @@ +import { SandboxApprovalMessage } from 'common'; import { Sandbox } from '../src/index.ts'; const textarea = document.getElementById('md') as HTMLTextAreaElement; const sandbox = new Sandbox(document.body, textarea.value, { @@ -9,12 +10,12 @@ const sandbox = new Sandbox(document.body, textarea.value, { }, onApprove: (message) => { console.log('Sandbox approval message:', message); - return { + const approval: SandboxApprovalMessage = { type: 'sandboxApproval', transactionId: message.transactionId, - approved: true, remediated: message.flags, }; + return approval; } }); diff --git a/packages/vscode-resources/src/html-json.ts b/packages/vscode-resources/src/html-json.ts index 8ff103e1..b1ab7ad9 100644 --- a/packages/vscode-resources/src/html-json.ts +++ b/packages/vscode-resources/src/html-json.ts @@ -25,7 +25,6 @@ window.addEventListener('DOMContentLoaded', () => { const sandboxedApprovalMessage: IDocs.common.SandboxApprovalMessage = { type: 'sandboxApproval', transactionId: message.transactionId, - approved: true, remediated, }; return sandboxedApprovalMessage; diff --git a/packages/vscode-resources/src/html-markdown.ts b/packages/vscode-resources/src/html-markdown.ts index 6829b1d2..b2bf04cd 100644 --- a/packages/vscode-resources/src/html-markdown.ts +++ b/packages/vscode-resources/src/html-markdown.ts @@ -10,7 +10,6 @@ window.addEventListener('DOMContentLoaded', () => { const sandboxedApprovalMessage: IDocs.common.SandboxApprovalMessage = { type: 'sandboxApproval', transactionId: message.transactionId, - approved: true, remediated, }; return sandboxedApprovalMessage; From ce4bf4c2e042d24d4337684bd2a0f44cec18ada1 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 21:49:22 -0700 Subject: [PATCH 10/20] remove unused --- packages/markdown/src/plugins/vega-lite.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/markdown/src/plugins/vega-lite.ts b/packages/markdown/src/plugins/vega-lite.ts index e1cdf92b..89609a3c 100644 --- a/packages/markdown/src/plugins/vega-lite.ts +++ b/packages/markdown/src/plugins/vega-lite.ts @@ -14,14 +14,6 @@ import { Spec } from 'vega'; const pluginName = 'vega-lite'; const className = pluginClassName(pluginName); -export function inspectVegaLiteSpec(spec: TopLevelSpec) { - //TODO inspect spec for flags, such as http:// instead of https://, or other security issues - const flaggableSpec: FlaggableSpec = { - spec, - }; - return flaggableSpec; -} - export const vegaLitePlugin: Plugin = { ...flaggableJsonPlugin(pluginName, className), fence: (token, index) => { From d0cd693d816ba525fe260a5ccad04530567dc347 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 21:57:10 -0700 Subject: [PATCH 11/20] rename to RawFlaggableSpec --- packages/markdown/src/factory.ts | 4 ++-- packages/markdown/src/plugins/config.ts | 6 +++--- packages/markdown/src/plugins/css.ts | 4 ++-- packages/markdown/src/plugins/vega-lite.ts | 4 ++-- packages/markdown/src/plugins/vega.ts | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/markdown/src/factory.ts b/packages/markdown/src/factory.ts index acd69434..f5906805 100644 --- a/packages/markdown/src/factory.ts +++ b/packages/markdown/src/factory.ts @@ -37,7 +37,7 @@ export interface IInstance { getCurrentSignalValue?: (signalName: string) => unknown; } -export interface FlaggableSpec { +export interface RawFlaggableSpec { spec: T; hasFlags?: boolean; reason?: string; @@ -45,7 +45,7 @@ export interface FlaggableSpec { export interface SpecContainer { container: HTMLElement; - flaggableSpec: FlaggableSpec; + flaggableSpec: RawFlaggableSpec; } export interface Plugin { diff --git a/packages/markdown/src/plugins/config.ts b/packages/markdown/src/plugins/config.ts index aeb9e42b..332399df 100644 --- a/packages/markdown/src/plugins/config.ts +++ b/packages/markdown/src/plugins/config.ts @@ -1,9 +1,9 @@ -import { definePlugin, Plugin, FlaggableSpec } from "../factory.js"; +import { definePlugin, Plugin, RawFlaggableSpec } from "../factory.js"; import { sanitizedHTML } from "../sanitize.js"; import { getJsonScriptTag } from "./util.js"; import { Flagged } from 'common'; -export function flaggableJsonPlugin(pluginName: string, className: string, flagger?: (spec: T) => FlaggableSpec) { +export function flaggableJsonPlugin(pluginName: string, className: string, flagger?: (spec: T) => RawFlaggableSpec) { const plugin: Plugin = { name: pluginName, initializePlugin: (md) => definePlugin(md, pluginName), @@ -11,7 +11,7 @@ export function flaggableJsonPlugin(pluginName: string, className: string, fl let json = token.content.trim(); if (flagger) { let spec: T; - let flaggableSpec: FlaggableSpec; + let flaggableSpec: RawFlaggableSpec; try { spec = JSON.parse(json); } catch (e) { diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index dbc29578..b991d99b 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { definePlugin, IInstance, Plugin, FlaggableSpec } from '../factory.js'; +import { definePlugin, IInstance, Plugin, RawFlaggableSpec } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import * as Csstree from 'css-tree'; import { pluginClassName } from './util.js'; @@ -88,7 +88,7 @@ function categorizeCss(cssContent: string) { atRules: {}, }; - const result: FlaggableSpec = { + const result: RawFlaggableSpec = { spec, hasFlags: false }; diff --git a/packages/markdown/src/plugins/vega-lite.ts b/packages/markdown/src/plugins/vega-lite.ts index 89609a3c..de16e4de 100644 --- a/packages/markdown/src/plugins/vega-lite.ts +++ b/packages/markdown/src/plugins/vega-lite.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { Plugin, FlaggableSpec } from '../factory.js'; +import { Plugin, RawFlaggableSpec } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { flaggableJsonPlugin } from './config.js'; import { pluginClassName } from './util.js'; @@ -19,7 +19,7 @@ export const vegaLitePlugin: Plugin = { fence: (token, index) => { let json = token.content.trim(); let spec: TopLevelSpec; - let flaggableSpec: FlaggableSpec; + let flaggableSpec: RawFlaggableSpec; try { spec = JSON.parse(json); } catch (e) { diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index 772836f5..0d65285c 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -4,7 +4,7 @@ */ import { changeset, parse, View, expressionFunction, LoggerInterface } from 'vega'; -import { Batch, IInstance, Plugin, PrioritizedSignal, FlaggableSpec } from '../factory.js'; +import { Batch, IInstance, Plugin, PrioritizedSignal, RawFlaggableSpec } from '../factory.js'; import { BaseSignal, InitSignal, NewSignal, Runtime, Spec, ValuesData } from 'vega-typings'; import { ErrorHandler, Renderer } from '../renderer.js'; import { LogLevel } from '../signalbus.js'; @@ -34,7 +34,7 @@ const className = pluginClassName(pluginName); export function inspectVegaSpec(spec: Spec) { //TODO inspect spec for flags, such as http:// instead of https://, or other security issues - const flaggableSpec: FlaggableSpec = { + const flaggableSpec: RawFlaggableSpec = { spec, }; return flaggableSpec; From d149fd514061f3fe297e46758af86d8a20f8907f Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 23:21:58 -0700 Subject: [PATCH 12/20] spec approval --- docs/view/index.html | 11 +- docs/view/view.js | 15 + packages/common/src/types.ts | 4 +- packages/editor/src/app.tsx | 264 +++++++++--------- packages/editor/src/editor.tsx | 16 +- packages/editor/src/sandbox.tsx | 17 +- packages/host/src/listener.ts | 19 +- packages/host/src/post-receive.ts | 2 +- packages/markdown/src/plugins/config.ts | 11 +- packages/markdown/src/plugins/css.ts | 11 +- packages/markdown/src/plugins/vega.ts | 5 +- packages/sandbox-resources/src/sandboxed.ts | 2 +- packages/sandbox/dev/index.ts | 9 +- packages/sandbox/src/preview.ts | 4 +- packages/sandbox/src/sandbox.ts | 19 +- packages/vscode-resources/package.json | 4 +- packages/vscode-resources/src/edit.ts | 14 +- packages/vscode-resources/src/html-json.ts | 10 +- .../vscode-resources/src/html-markdown.ts | 10 +- packages/vscode-resources/src/preview.ts | 8 + packages/vscode-resources/src/view.ts | 17 ++ 21 files changed, 249 insertions(+), 223 deletions(-) create mode 100644 docs/view/view.js create mode 100644 packages/vscode-resources/src/view.ts diff --git a/docs/view/index.html b/docs/view/index.html index 68394971..a95cd9fc 100644 --- a/docs/view/index.html +++ b/docs/view/index.html @@ -87,16 +87,7 @@

Interactive Document Host

- + diff --git a/docs/view/view.js b/docs/view/view.js new file mode 100644 index 00000000..90826f87 --- /dev/null +++ b/docs/view/view.js @@ -0,0 +1,15 @@ +window.addEventListener('DOMContentLoaded', () => { + host = new IDocs.host.Listener({ + app: 'main', + loading: '#loading', + help: '#help', + uploadButton: '#upload-btn', + fileInput: '#file-input', + textarea: '#textarea', + onApprove: (message) => { + // TODO look through each and override policy to approve unapproved + const remediated = message.flags; + return remediated; + }, + }); +}); diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index 8ff1a47d..c22635f4 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -1,7 +1,7 @@ export interface Flagged { pluginName: string; containerId: string; - spec: T; - hasFlags?: boolean; + approvedSpec: T; + unApprovedSpec?: T; reason?: string; } diff --git a/packages/editor/src/app.tsx b/packages/editor/src/app.tsx index aecf5c11..678ad5fe 100644 --- a/packages/editor/src/app.tsx +++ b/packages/editor/src/app.tsx @@ -1,157 +1,159 @@ import { InteractiveDocument } from "schema"; import { Editor } from './editor.js'; import { Previewer } from '@microsoft/chartifact-sandbox'; -import { EditorPageMessage, EditorReadyMessage } from "common"; +import { EditorPageMessage, EditorReadyMessage, Flagged, SandboxedPreHydrateMessage } from "common"; export interface AppProps { - previewer: typeof Previewer; + previewer: typeof Previewer; + onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; } // Alternative implementation using same-origin communication export function App(props: AppProps) { - const { previewer } = props; + const { previewer } = props; - const [history, setHistory] = React.useState([initialPage]); - const [historyIndex, setHistoryIndex] = React.useState(0); - const [currentPage, setCurrentPage] = React.useState(initialPage); + const [history, setHistory] = React.useState([initialPage]); + const [historyIndex, setHistoryIndex] = React.useState(0); + const [currentPage, setCurrentPage] = React.useState(initialPage); - const editorContainerRef = React.useRef(null); - const [isEditorReady, setIsEditorReady] = React.useState(false); + const editorContainerRef = React.useRef(null); + const [isEditorReady, setIsEditorReady] = React.useState(false); - const undo = () => { - if (historyIndex > 0) { - const newIndex = historyIndex - 1; - setHistoryIndex(newIndex); - const page = history[newIndex]; - setCurrentPage(page); - sendPageToEditor(page); - } - }; + const undo = () => { + if (historyIndex > 0) { + const newIndex = historyIndex - 1; + setHistoryIndex(newIndex); + const page = history[newIndex]; + setCurrentPage(page); + sendPageToEditor(page); + } + }; - const redo = () => { - if (historyIndex < history.length - 1) { - const newIndex = historyIndex + 1; - setHistoryIndex(newIndex); - const page = history[newIndex]; - setCurrentPage(page); - sendPageToEditor(page); - } - }; + const redo = () => { + if (historyIndex < history.length - 1) { + const newIndex = historyIndex + 1; + setHistoryIndex(newIndex); + const page = history[newIndex]; + setCurrentPage(page); + sendPageToEditor(page); + } + }; - const sendPageToEditor = (page: InteractiveDocument, skipReadyCheck = false) => { - // Only send page if editor is ready (unless we're skipping the check) - if (!skipReadyCheck && !isEditorReady) { - return; - } + const sendPageToEditor = (page: InteractiveDocument, skipReadyCheck = false) => { + // Only send page if editor is ready (unless we're skipping the check) + if (!skipReadyCheck && !isEditorReady) { + return; + } - // Post message to the editor within the same window - const pageMessage: EditorPageMessage = { - type: 'editorPage', - page: page, - sender: 'app', - }; - window.postMessage(pageMessage, '*'); + // Post message to the editor within the same window + const pageMessage: EditorPageMessage = { + type: 'editorPage', + page: page, + sender: 'app', }; + window.postMessage(pageMessage, '*'); + }; - React.useEffect(() => { - // Listen for messages from the editor - const handleMessage = (event: MessageEvent) => { - // Only process messages from editor, ignore our own messages - if (event.data && event.data.sender === 'editor') { - if (event.data.type === 'editorReady') { - setIsEditorReady(true); - // Send initial page when editor is ready - sendPageToEditor(currentPage); - } else if (event.data.type === 'editorPage' && event.data.page) { - const pageMessage = event.data as EditorPageMessage; - // Use functional updates to avoid closure issues - setHistoryIndex(prevIndex => { - setHistory(prevHistory => { - // Truncate history after current index and add new page - const newHistory = prevHistory.slice(0, prevIndex + 1); - newHistory.push(pageMessage.page); - return newHistory; - }); + React.useEffect(() => { + // Listen for messages from the editor + const handleMessage = (event: MessageEvent) => { + // Only process messages from editor, ignore our own messages + if (event.data && event.data.sender === 'editor') { + if (event.data.type === 'editorReady') { + setIsEditorReady(true); + // Send initial page when editor is ready + sendPageToEditor(currentPage); + } else if (event.data.type === 'editorPage' && event.data.page) { + const pageMessage = event.data as EditorPageMessage; + // Use functional updates to avoid closure issues + setHistoryIndex(prevIndex => { + setHistory(prevHistory => { + // Truncate history after current index and add new page + const newHistory = prevHistory.slice(0, prevIndex + 1); + newHistory.push(pageMessage.page); + return newHistory; + }); - setCurrentPage(pageMessage.page); - // Send the updated page back to the editor (skip ready check since editor just sent us a message) - sendPageToEditor(pageMessage.page, true); - return prevIndex + 1; // New index will be the last item - }); - } - } - }; - - window.addEventListener('message', handleMessage); + setCurrentPage(pageMessage.page); + // Send the updated page back to the editor (skip ready check since editor just sent us a message) + sendPageToEditor(pageMessage.page, true); + return prevIndex + 1; // New index will be the last item + }); + } + } + }; - return () => { - window.removeEventListener('message', handleMessage); - }; - }, []); // Remove the dependencies that were causing stale closures + window.addEventListener('message', handleMessage); - React.useEffect(() => { - // This effect runs when isEditorReady changes - // If editor becomes ready, send current page - if (isEditorReady) { - sendPageToEditor(currentPage); - } - }, [isEditorReady]); + return () => { + window.removeEventListener('message', handleMessage); + }; + }, []); // Remove the dependencies that were causing stale closures - return ( -
- {/* Control Panel */} -
-

Document Editor

-
- - - - History: {historyIndex + 1} / {history.length} - -
-
+ React.useEffect(() => { + // This effect runs when isEditorReady changes + // If editor becomes ready, send current page + if (isEditorReady) { + sendPageToEditor(currentPage); + } + }, [isEditorReady]); - {/* Editor */} -
- -
+ return ( +
+ {/* Control Panel */} +
+

Document Editor

+
+ + + + History: {historyIndex + 1} / {history.length} +
- ); +
+ + {/* Editor */} +
+ +
+
+ ); } const initialPage: InteractiveDocument = { diff --git a/packages/editor/src/editor.tsx b/packages/editor/src/editor.tsx index 1c42bf8b..592cdd97 100644 --- a/packages/editor/src/editor.tsx +++ b/packages/editor/src/editor.tsx @@ -1,11 +1,12 @@ import { InteractiveDocument } from "schema"; import { SandboxDocumentPreview } from "./sandbox.js"; import { Previewer } from '@microsoft/chartifact-sandbox'; -import { EditorPageMessage, EditorReadyMessage } from "common"; +import { EditorPageMessage, EditorReadyMessage, Flagged, SandboxedPreHydrateMessage } from "common"; export interface EditorProps { postMessageTarget?: Window; previewer: typeof Previewer; + onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; } const devmode = false; // Set to true to use DevDocumentPreview, false for SandboxDocumentPreview @@ -63,17 +64,25 @@ export function Editor(props: EditorProps) { postMessageTarget.postMessage(readyMessage, '*'); }, []); - return ; + return ( + + ); } export interface EditorViewProps { page: InteractiveDocument; postMessageTarget: Window; previewer: typeof Previewer; + onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; } export function EditorView(props: EditorViewProps) { - const { page, postMessageTarget, previewer } = props; + const { page, postMessageTarget, previewer, onApprove } = props; const sendEditToApp = (newPage: InteractiveDocument) => { const pageMessage: EditorPageMessage = { @@ -188,6 +197,7 @@ export function EditorView(props: EditorViewProps) {
diff --git a/packages/editor/src/sandbox.tsx b/packages/editor/src/sandbox.tsx index 11ebf5c1..aa9ac493 100644 --- a/packages/editor/src/sandbox.tsx +++ b/packages/editor/src/sandbox.tsx @@ -2,11 +2,12 @@ import React from 'react'; import { InteractiveDocument } from "schema"; import { targetMarkdown } from '@microsoft/interactive-document-compiler'; import { Previewer, Sandbox } from '@microsoft/chartifact-sandbox'; -import { SandboxApprovalMessage, SandboxedPreHydrateMessage } from 'common'; +import { Flagged, SandboxedPreHydrateMessage } from 'common'; export interface SandboxDocumentPreviewProps { page: InteractiveDocument; previewer?: typeof Previewer; + onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; } export class SandboxDocumentPreview extends React.Component { @@ -43,19 +44,7 @@ export class SandboxDocumentPreview extends React.Component console.error('Sandbox initialization failed:', error), - onApprove: (message: SandboxedPreHydrateMessage) => { - // Handle sandboxed pre-render message - console.log('Handling sandboxed pre-render message:', message); - const remediated = message.flags; - - // Approve the sandboxed pre-render - const sandboxedApprovalMessage: SandboxApprovalMessage = { - type: 'sandboxApproval', - transactionId: message.transactionId, - remediated, - }; - return sandboxedApprovalMessage; - }, + onApprove: this.props.onApprove, } ); } catch (error) { diff --git a/packages/host/src/listener.ts b/packages/host/src/listener.ts index aa080678..a2082493 100644 --- a/packages/host/src/listener.ts +++ b/packages/host/src/listener.ts @@ -8,7 +8,7 @@ import { setupPostMessageHandling } from './post-receive.js'; import { InteractiveDocument, InteractiveDocumentWithSchema } from 'schema'; import { postStatus } from './post-send.js'; import { ListenOptions } from './types.js'; -import { SandboxApprovalMessage } from 'common/dist/esnext/messages.js'; +import { Flagged, SandboxedPreHydrateMessage } from 'common'; function getElement(elementOrSelector: string | T): T | null { if (typeof elementOrSelector === 'string') { @@ -32,6 +32,7 @@ export interface InitializeOptions { fileInput?: string | HTMLElement; textarea?: string | HTMLTextAreaElement; options?: ListenOptions; + onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; } const defaultOptions: ListenOptions = { @@ -53,12 +54,14 @@ export class Listener { public fileInput: HTMLElement; public textarea: HTMLTextAreaElement; public sandbox: Sandbox; + public onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; private removeInteractionHandlers: (() => void)[]; private sandboxReady: boolean = false; constructor(options: InitializeOptions) { this.options = { ...defaultOptions, ...options?.options }; + this.onApprove = options.onApprove; this.removeInteractionHandlers = []; this.appDiv = getElement(options.app); @@ -116,19 +119,7 @@ export class Listener { onError: () => { this.errorHandler(new Error('Sandbox initialization failed'), 'Sandbox could not be initialized'); }, - onApprove: (message) => { - // Handle sandboxed pre-render message - console.log('Handling sandboxed pre-render message:', message); - const remediated = message.flags; - - // Approve the sandboxed pre-render - const sandboxedApprovalMessage: SandboxApprovalMessage = { - type: 'sandboxApproval', - transactionId: message.transactionId, - remediated, - }; - return sandboxedApprovalMessage; - }, + onApprove: this.onApprove, }); } diff --git a/packages/host/src/post-receive.ts b/packages/host/src/post-receive.ts index 33916f2e..22465934 100644 --- a/packages/host/src/post-receive.ts +++ b/packages/host/src/post-receive.ts @@ -1,5 +1,5 @@ import { Listener } from './listener.js'; -import type { HostRenderRequestMessage, SandboxApprovalMessage, SandboxedPreHydrateMessage } from 'common'; +import type { HostRenderRequestMessage } from 'common'; export function setupPostMessageHandling(host: Listener) { window.addEventListener('message', (event) => { diff --git a/packages/markdown/src/plugins/config.ts b/packages/markdown/src/plugins/config.ts index 332399df..a9f11880 100644 --- a/packages/markdown/src/plugins/config.ts +++ b/packages/markdown/src/plugins/config.ts @@ -35,9 +35,16 @@ export function flaggableJsonPlugin(pluginName: string, className: string, fl const containers = renderer.element.querySelectorAll(`.${className}`); for (const [index, container] of Array.from(containers).entries()) { const id = container.id; - const flaggableSpec = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); + const flaggableSpec = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)) as RawFlaggableSpec; if (!flaggableSpec) continue; - flagged.push({ ...flaggableSpec, pluginName, containerId: container.id }); + const f: Flagged = { approvedSpec: null, pluginName, containerId: container.id }; + if (flaggableSpec.hasFlags) { + f.unApprovedSpec = flaggableSpec.spec; + f.reason = flaggableSpec.reason; + } else { + f.approvedSpec = flaggableSpec.spec; + } + flagged.push(f); } return flagged; }, diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index b991d99b..7a20c691 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -374,17 +374,14 @@ export const cssPlugin: Plugin = { for (let index = 0; index < configContainers.length; index++) { const configContainer = configContainers[index]; + if (!configContainer.approvedSpec) { + continue; + } const container = renderer.element.querySelector(`#${configContainer.containerId}`); - const categorizedCss = configContainer.spec; + const categorizedCss = configContainer.approvedSpec; const comments: string[] = []; - // Log security issues found - if (configContainer.hasFlags) { - console.warn(`CSS security: Security issues detected in CSS`); - comments.push(``); - } - // Generate and apply safe CSS const safeCss = reconstituteCss(categorizedCss.atRules); diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index 0d65285c..f8608d3d 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -53,8 +53,11 @@ export const vegaPlugin: Plugin = { const specInits: SpecInit[] = []; for (let index = 0; index < configContainers.length; index++) { const configContainer = configContainers[index]; + if (!configContainer.approvedSpec) { + continue; + } const container = renderer.element.querySelector(`#${configContainer.containerId}`); - const specInit = createSpecInit(container, index, configContainer.spec); + const specInit = createSpecInit(container, index, configContainer.approvedSpec); if (specInit) { specInits.push(specInit); } diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index c4242605..0ad6ac8d 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -61,7 +61,7 @@ document.addEventListener('DOMContentLoaded', () => { //only handle if the transactionId is the latest if (message.transactionId === transactionIndex - 1) { - //todo: mutate the document according to approval + //todo: console.warn of unapproved //hydrate the renderer const flags = transactions[message.transactionId]; diff --git a/packages/sandbox/dev/index.ts b/packages/sandbox/dev/index.ts index a4d3b09d..7e0676ad 100644 --- a/packages/sandbox/dev/index.ts +++ b/packages/sandbox/dev/index.ts @@ -1,4 +1,3 @@ -import { SandboxApprovalMessage } from 'common'; import { Sandbox } from '../src/index.ts'; const textarea = document.getElementById('md') as HTMLTextAreaElement; const sandbox = new Sandbox(document.body, textarea.value, { @@ -10,12 +9,8 @@ const sandbox = new Sandbox(document.body, textarea.value, { }, onApprove: (message) => { console.log('Sandbox approval message:', message); - const approval: SandboxApprovalMessage = { - type: 'sandboxApproval', - transactionId: message.transactionId, - remediated: message.flags, - }; - return approval; + //TODO policy to approve unapproved + return message.flags; } }); diff --git a/packages/sandbox/src/preview.ts b/packages/sandbox/src/preview.ts index cfdb552c..373b7617 100644 --- a/packages/sandbox/src/preview.ts +++ b/packages/sandbox/src/preview.ts @@ -1,9 +1,9 @@ -import { SandboxApprovalMessage, SandboxedPreHydrateMessage } from "common/dist/esnext/messages.js"; +import { Flagged, SandboxedPreHydrateMessage } from "common"; export interface PreviewerOptions { onReady?: () => void; onError?: (error: Error) => void; - onApprove?: (message: SandboxedPreHydrateMessage) => SandboxApprovalMessage; + onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; } // Previewer class diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index 7a96cf30..c91708c6 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -2,12 +2,12 @@ import { Previewer, PreviewerOptions } from './preview.js'; import { rendererHtml } from './resources/rendererHtml.js'; import { rendererUmdJs } from './resources/rendererUmdJs.js'; import { sandboxedJs } from './resources/sandboxedJs.js'; -import type { SandboxRenderMessage, SandboxApprovalMessage, SandboxedPreHydrateMessage } from 'common'; +import type { SandboxRenderMessage, SandboxedPreHydrateMessage, SandboxApprovalMessage } from 'common'; export class Sandbox extends Previewer { public iframe: HTMLIFrameElement; - constructor(elementOrSelector: string | HTMLElement, markdown: string, options?: PreviewerOptions) { + constructor(elementOrSelector: string | HTMLElement, markdown: string, public options: PreviewerOptions) { super(elementOrSelector, markdown, options); const renderRequest: SandboxRenderMessage = { @@ -29,16 +29,17 @@ export class Sandbox extends Previewer { }); window.addEventListener('message', (event) => { + //make sure its from the sandbox iframe if (event.source === this.iframe.contentWindow) { const message = event.data as SandboxedPreHydrateMessage; if (message.type == 'sandboxedPreHydrate') { - //make sure its from the sandbox iframe - if (this.options?.onApprove) { - // TODO check whitelist and do a mutation if needed - // TODO loop through all flagged - const sandboxedApprovalMessage = this.options.onApprove(message); - this.iframe.contentWindow?.postMessage(sandboxedApprovalMessage, '*'); - } + const remediated = this.options.onApprove(message); + const sandboxedApprovalMessage: SandboxApprovalMessage = { + type: 'sandboxApproval', + transactionId: message.transactionId, + remediated, + }; + this.iframe.contentWindow?.postMessage(sandboxedApprovalMessage, '*'); } } }); diff --git a/packages/vscode-resources/package.json b/packages/vscode-resources/package.json index ef07672f..de79430b 100644 --- a/packages/vscode-resources/package.json +++ b/packages/vscode-resources/package.json @@ -6,7 +6,9 @@ "scripts": { "clean": "rimraf dist", "tsc": "tsc -p .", - "build:07": "npm run tsc" + "build": "npm run tsc", + "postbuild": "node -e \"const fs = require('fs'); const path = require('path'); const src = path.join(__dirname, 'dist', 'view.js'); const destDir = path.join(__dirname, '..', '..', 'docs', 'view'); const dest = path.join(destDir, 'view.js'); fs.mkdirSync(destDir, { recursive: true }); fs.copyFileSync(src, dest); console.log('Copied view.js to docs/view');\"", + "build:07": "npm run build" }, "author": "Dan Marshall", "license": "MIT", diff --git a/packages/vscode-resources/src/edit.ts b/packages/vscode-resources/src/edit.ts index fac6e9d6..cd39b816 100644 --- a/packages/vscode-resources/src/edit.ts +++ b/packages/vscode-resources/src/edit.ts @@ -18,9 +18,19 @@ window.addEventListener('DOMContentLoaded', () => { return offlineDeps; } } - const editorProps = { previewer: OfflineSandbox, postMessageTarget: vscode }; + const editorProps: IDocs.editor.EditorProps = { + previewer: OfflineSandbox, + postMessageTarget: vscode as any, + onApprove: (message) => { + // Handle sandboxed pre-render message + console.log('Handling sandboxed pre-render message:', message); + //Here you can approve unapproved specs per your own policy + const remediated = message.flags; + return remediated; + } + }; const root = ReactDOM.createRoot(document.getElementById("app")); - root.render(React.createElement(IDocs.editor.Editor, editorProps as any)); + root.render(React.createElement(IDocs.editor.Editor, editorProps)); } }); diff --git a/packages/vscode-resources/src/html-json.ts b/packages/vscode-resources/src/html-json.ts index b1ab7ad9..ff7ce69d 100644 --- a/packages/vscode-resources/src/html-json.ts +++ b/packages/vscode-resources/src/html-json.ts @@ -19,15 +19,9 @@ window.addEventListener('DOMContentLoaded', () => { onApprove: (message) => { // Handle sandboxed pre-render message console.log('Handling sandboxed pre-render message:', message); + //Here you can approve unapproved specs per your own policy const remediated = message.flags; - - // Approve the sandboxed pre-render - const sandboxedApprovalMessage: IDocs.common.SandboxApprovalMessage = { - type: 'sandboxApproval', - transactionId: message.transactionId, - remediated, - }; - return sandboxedApprovalMessage; + return remediated; }, onError: (error) => { console.error('Sandbox error:', error); diff --git a/packages/vscode-resources/src/html-markdown.ts b/packages/vscode-resources/src/html-markdown.ts index b2bf04cd..38a6d73a 100644 --- a/packages/vscode-resources/src/html-markdown.ts +++ b/packages/vscode-resources/src/html-markdown.ts @@ -4,15 +4,9 @@ window.addEventListener('DOMContentLoaded', () => { onApprove: (message) => { // Handle sandboxed pre-render message console.log('Handling sandboxed pre-render message:', message); + //Here you can approve unapproved specs per your own policy const remediated = message.flags; - - // Approve the sandboxed pre-render - const sandboxedApprovalMessage: IDocs.common.SandboxApprovalMessage = { - type: 'sandboxApproval', - transactionId: message.transactionId, - remediated, - }; - return sandboxedApprovalMessage; + return remediated; }, onError: (error) => { console.error('Sandbox error:', error); diff --git a/packages/vscode-resources/src/preview.ts b/packages/vscode-resources/src/preview.ts index b304f0a0..1a089490 100644 --- a/packages/vscode-resources/src/preview.ts +++ b/packages/vscode-resources/src/preview.ts @@ -6,5 +6,13 @@ window.addEventListener('DOMContentLoaded', () => { app: '#app', loading: '#loading', options, + onApprove: (message: IDocs.common.SandboxedPreHydrateMessage) => { + // Handle sandboxed pre-render message + console.log('Handling sandboxed pre-render message:', message); + + // TODO look through each and override policy to approve unapproved + const remediated = message.flags; + return remediated; + }, }); }); diff --git a/packages/vscode-resources/src/view.ts b/packages/vscode-resources/src/view.ts new file mode 100644 index 00000000..a5849af0 --- /dev/null +++ b/packages/vscode-resources/src/view.ts @@ -0,0 +1,17 @@ +declare let host: IDocs.host.Listener; + +window.addEventListener('DOMContentLoaded', () => { + host = new IDocs.host.Listener({ + app: 'main', + loading: '#loading', + help: '#help', + uploadButton: '#upload-btn', + fileInput: '#file-input', + textarea: '#textarea', + onApprove: (message: IDocs.common.SandboxedPreHydrateMessage) => { + // TODO look through each and override policy to approve unapproved + const remediated = message.flags; + return remediated; + }, + }); +}); From 254e6b6d97673d9d429cb77a2d2e787060b94386 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 05:07:10 -0700 Subject: [PATCH 13/20] collect reasons --- packages/common/src/index.ts | 1 - packages/common/src/messages.ts | 9 ++++++++- packages/common/src/types.ts | 7 ------- packages/markdown/src/factory.ts | 2 +- packages/markdown/src/plugins/config.ts | 6 +++--- packages/markdown/src/plugins/css.ts | 9 +++++++-- packages/markdown/src/plugins/vega-lite.ts | 4 ++-- 7 files changed, 21 insertions(+), 17 deletions(-) delete mode 100644 packages/common/src/types.ts diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 75c62e56..4bb11d8c 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,3 +1,2 @@ export * from './common.js'; export * from './messages.js'; -export * from './types.js'; diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index 0012799e..2fc39794 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -1,11 +1,18 @@ import { InteractiveDocument } from "schema"; -import { Flagged } from "./types.js"; export interface SandboxRenderMessage { type: 'sandboxRender'; markdown?: string; } +export interface Flagged { + pluginName: string; + containerId: string; + approvedSpec: T; + blockedSpec?: T; + reason?: string; +} + export interface SandboxedPreHydrateMessage { type: 'sandboxedPreHydrate'; transactionId: number; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts deleted file mode 100644 index c22635f4..00000000 --- a/packages/common/src/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface Flagged { - pluginName: string; - containerId: string; - approvedSpec: T; - unApprovedSpec?: T; - reason?: string; -} diff --git a/packages/markdown/src/factory.ts b/packages/markdown/src/factory.ts index f5906805..23f43aa1 100644 --- a/packages/markdown/src/factory.ts +++ b/packages/markdown/src/factory.ts @@ -40,7 +40,7 @@ export interface IInstance { export interface RawFlaggableSpec { spec: T; hasFlags?: boolean; - reason?: string; + reasons?: string[]; } export interface SpecContainer { diff --git a/packages/markdown/src/plugins/config.ts b/packages/markdown/src/plugins/config.ts index a9f11880..1772387d 100644 --- a/packages/markdown/src/plugins/config.ts +++ b/packages/markdown/src/plugins/config.ts @@ -18,7 +18,7 @@ export function flaggableJsonPlugin(pluginName: string, className: string, fl flaggableSpec = { spec: null, hasFlags: true, - reason: `malformed JSON` + reasons: [`malformed JSON`], }; } if (spec) { @@ -39,8 +39,8 @@ export function flaggableJsonPlugin(pluginName: string, className: string, fl if (!flaggableSpec) continue; const f: Flagged = { approvedSpec: null, pluginName, containerId: container.id }; if (flaggableSpec.hasFlags) { - f.unApprovedSpec = flaggableSpec.spec; - f.reason = flaggableSpec.reason; + f.blockedSpec = flaggableSpec.spec; + f.reason = flaggableSpec.reasons?.join(', ') || 'Unknown reason'; } else { f.approvedSpec = flaggableSpec.spec; } diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index 7a20c691..3c254408 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -90,7 +90,8 @@ function categorizeCss(cssContent: string) { const result: RawFlaggableSpec = { spec, - hasFlags: false + hasFlags: false, + reasons: [], }; // At-rules that should be treated as complete blocks (not parsed internally) @@ -210,13 +211,15 @@ function categorizeCss(cssContent: string) { // Check for @import specifically if (node.name === 'import') { const ruleContent = csstree.generate(node); + const reason = '@import rule detected - requires approval'; spec.atRules[atRuleSignature] = { signature: atRuleSignature, css: ruleContent, flag: 'importRule', - reason: '@import rule detected - requires approval' + reason, }; result.hasFlags = true; + result.reasons.push(reason); return; } @@ -274,6 +277,7 @@ function categorizeCss(cssContent: string) { declaration.flag = securityCheck.flag; declaration.reason = securityCheck.reason; result.hasFlags = true; + result.reasons.push(securityCheck.reason); } currentRule.declarations.push(declaration); @@ -291,6 +295,7 @@ function categorizeCss(cssContent: string) { lastDecl.flag = securityCheck.flag; lastDecl.reason = securityCheck.reason; result.hasFlags = true; + result.reasons.push(securityCheck.reason); } } } diff --git a/packages/markdown/src/plugins/vega-lite.ts b/packages/markdown/src/plugins/vega-lite.ts index de16e4de..e7d9dd71 100644 --- a/packages/markdown/src/plugins/vega-lite.ts +++ b/packages/markdown/src/plugins/vega-lite.ts @@ -26,7 +26,7 @@ export const vegaLitePlugin: Plugin = { flaggableSpec = { spec: null, hasFlags: true, - reason: `malformed JSON` + reasons: [`malformed JSON`], }; } if (spec) { @@ -38,7 +38,7 @@ export const vegaLitePlugin: Plugin = { flaggableSpec = { spec: null, hasFlags: true, - reason: `failed to compile vega spec` + reasons: [`failed to compile vega spec`], }; } } From dbc863954b843574e8ccfb3a3a5ca783c85b1b39 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 05:13:44 -0700 Subject: [PATCH 14/20] rename to SpecReview --- packages/common/src/messages.ts | 6 +++--- packages/editor/src/app.tsx | 4 ++-- packages/editor/src/editor.tsx | 6 +++--- packages/editor/src/sandbox.tsx | 4 ++-- packages/host/src/listener.ts | 6 +++--- packages/markdown/src/factory.ts | 6 +++--- packages/markdown/src/plugins/config.ts | 6 +++--- packages/markdown/src/renderer.ts | 6 +++--- packages/sandbox-resources/src/sandboxed.ts | 2 +- packages/sandbox/src/preview.ts | 4 ++-- 10 files changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index 2fc39794..afb87e5f 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -5,7 +5,7 @@ export interface SandboxRenderMessage { markdown?: string; } -export interface Flagged { +export interface SpecReview { pluginName: string; containerId: string; approvedSpec: T; @@ -16,13 +16,13 @@ export interface Flagged { export interface SandboxedPreHydrateMessage { type: 'sandboxedPreHydrate'; transactionId: number; - flags: Flagged<{}>[]; + flags: SpecReview<{}>[]; } export type SandboxApprovalMessage = { type: 'sandboxApproval'; transactionId: number; - remediated: Flagged<{}>[]; + remediated: SpecReview<{}>[]; }; export interface HostRenderRequestMessage { diff --git a/packages/editor/src/app.tsx b/packages/editor/src/app.tsx index 678ad5fe..72f5e3d1 100644 --- a/packages/editor/src/app.tsx +++ b/packages/editor/src/app.tsx @@ -1,11 +1,11 @@ import { InteractiveDocument } from "schema"; import { Editor } from './editor.js'; import { Previewer } from '@microsoft/chartifact-sandbox'; -import { EditorPageMessage, EditorReadyMessage, Flagged, SandboxedPreHydrateMessage } from "common"; +import { EditorPageMessage, EditorReadyMessage, SpecReview, SandboxedPreHydrateMessage } from "common"; export interface AppProps { previewer: typeof Previewer; - onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; + onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; } // Alternative implementation using same-origin communication diff --git a/packages/editor/src/editor.tsx b/packages/editor/src/editor.tsx index 592cdd97..76c9caa9 100644 --- a/packages/editor/src/editor.tsx +++ b/packages/editor/src/editor.tsx @@ -1,12 +1,12 @@ import { InteractiveDocument } from "schema"; import { SandboxDocumentPreview } from "./sandbox.js"; import { Previewer } from '@microsoft/chartifact-sandbox'; -import { EditorPageMessage, EditorReadyMessage, Flagged, SandboxedPreHydrateMessage } from "common"; +import { EditorPageMessage, EditorReadyMessage, SpecReview, SandboxedPreHydrateMessage } from "common"; export interface EditorProps { postMessageTarget?: Window; previewer: typeof Previewer; - onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; + onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; } const devmode = false; // Set to true to use DevDocumentPreview, false for SandboxDocumentPreview @@ -78,7 +78,7 @@ export interface EditorViewProps { page: InteractiveDocument; postMessageTarget: Window; previewer: typeof Previewer; - onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; + onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; } export function EditorView(props: EditorViewProps) { diff --git a/packages/editor/src/sandbox.tsx b/packages/editor/src/sandbox.tsx index aa9ac493..9635ffa2 100644 --- a/packages/editor/src/sandbox.tsx +++ b/packages/editor/src/sandbox.tsx @@ -2,12 +2,12 @@ import React from 'react'; import { InteractiveDocument } from "schema"; import { targetMarkdown } from '@microsoft/interactive-document-compiler'; import { Previewer, Sandbox } from '@microsoft/chartifact-sandbox'; -import { Flagged, SandboxedPreHydrateMessage } from 'common'; +import { SpecReview, SandboxedPreHydrateMessage } from 'common'; export interface SandboxDocumentPreviewProps { page: InteractiveDocument; previewer?: typeof Previewer; - onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; + onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; } export class SandboxDocumentPreview extends React.Component { diff --git a/packages/host/src/listener.ts b/packages/host/src/listener.ts index a2082493..1f8039d9 100644 --- a/packages/host/src/listener.ts +++ b/packages/host/src/listener.ts @@ -8,7 +8,7 @@ import { setupPostMessageHandling } from './post-receive.js'; import { InteractiveDocument, InteractiveDocumentWithSchema } from 'schema'; import { postStatus } from './post-send.js'; import { ListenOptions } from './types.js'; -import { Flagged, SandboxedPreHydrateMessage } from 'common'; +import { SpecReview, SandboxedPreHydrateMessage } from 'common'; function getElement(elementOrSelector: string | T): T | null { if (typeof elementOrSelector === 'string') { @@ -32,7 +32,7 @@ export interface InitializeOptions { fileInput?: string | HTMLElement; textarea?: string | HTMLTextAreaElement; options?: ListenOptions; - onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; + onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; } const defaultOptions: ListenOptions = { @@ -54,7 +54,7 @@ export class Listener { public fileInput: HTMLElement; public textarea: HTMLTextAreaElement; public sandbox: Sandbox; - public onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; + public onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; private removeInteractionHandlers: (() => void)[]; private sandboxReady: boolean = false; diff --git a/packages/markdown/src/factory.ts b/packages/markdown/src/factory.ts index 23f43aa1..d8bc3ff2 100644 --- a/packages/markdown/src/factory.ts +++ b/packages/markdown/src/factory.ts @@ -7,7 +7,7 @@ import MarkdownIt, { Token } from 'markdown-it/index.js'; import { attrs } from '@mdit/plugin-attrs'; import { container, MarkdownItContainerOptions } from '@mdit/plugin-container'; import { ErrorHandler, Renderer } from './renderer.js'; -import { defaultCommonOptions, Flagged } from 'common'; +import { defaultCommonOptions, SpecReview } from 'common'; declare const markdownit: typeof MarkdownIt; @@ -53,8 +53,8 @@ export interface Plugin { hydratesBefore?: string; initializePlugin: (md: MarkdownIt) => void; fence?: (token: Token, idx: number) => string; - hydrateSpecs?: (renderer: Renderer, errorHandler: ErrorHandler) => Flagged[]; - hydrateComponent?: (renderer: Renderer, errorHandler: ErrorHandler, flagged: Flagged[]) => Promise; + hydrateSpecs?: (renderer: Renderer, errorHandler: ErrorHandler) => SpecReview[]; + hydrateComponent?: (renderer: Renderer, errorHandler: ErrorHandler, flagged: SpecReview[]) => Promise; } export const plugins: Plugin[] = []; diff --git a/packages/markdown/src/plugins/config.ts b/packages/markdown/src/plugins/config.ts index 1772387d..c92e25a3 100644 --- a/packages/markdown/src/plugins/config.ts +++ b/packages/markdown/src/plugins/config.ts @@ -1,7 +1,7 @@ import { definePlugin, Plugin, RawFlaggableSpec } from "../factory.js"; import { sanitizedHTML } from "../sanitize.js"; import { getJsonScriptTag } from "./util.js"; -import { Flagged } from 'common'; +import { SpecReview } from 'common'; export function flaggableJsonPlugin(pluginName: string, className: string, flagger?: (spec: T) => RawFlaggableSpec) { const plugin: Plugin = { @@ -31,13 +31,13 @@ export function flaggableJsonPlugin(pluginName: string, className: string, fl return sanitizedHTML('div', { class: className, id: `${pluginName}-${index}` }, json, true); }, hydrateSpecs: (renderer, errorHandler) => { - const flagged: Flagged[] = []; + const flagged: SpecReview[] = []; const containers = renderer.element.querySelectorAll(`.${className}`); for (const [index, container] of Array.from(containers).entries()) { const id = container.id; const flaggableSpec = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)) as RawFlaggableSpec; if (!flaggableSpec) continue; - const f: Flagged = { approvedSpec: null, pluginName, containerId: container.id }; + const f: SpecReview = { approvedSpec: null, pluginName, containerId: container.id }; if (flaggableSpec.hasFlags) { f.blockedSpec = flaggableSpec.spec; f.reason = flaggableSpec.reasons?.join(', ') || 'Unknown reason'; diff --git a/packages/markdown/src/renderer.ts b/packages/markdown/src/renderer.ts index 171a30c1..e71684a7 100644 --- a/packages/markdown/src/renderer.ts +++ b/packages/markdown/src/renderer.ts @@ -7,7 +7,7 @@ import MarkdownIt from 'markdown-it'; import { Renderers } from 'vega-typings'; import { create, IInstance, plugins } from './factory.js'; import { SignalBus } from './signalbus.js'; -import { defaultCommonOptions, Flagged } from 'common'; +import { defaultCommonOptions, SpecReview } from 'common'; export interface ErrorHandler { (error: Error, pluginName: string, instanceIndex: number, phase: string, container: Element, detail?: string): void; @@ -90,7 +90,7 @@ export class Renderer { hydrateSpecs() { this.ensureMd(); - const allFlagged: Flagged<{}>[] = []; + const allFlagged: SpecReview<{}>[] = []; //loop through all the plugins and hydrate their specs and flag them id needed this.signalBus.log('Renderer', 'hydrate specs'); @@ -105,7 +105,7 @@ export class Renderer { return allFlagged; } - async hydrate(hydratedSpecs: Flagged<{}>[]) { + async hydrate(hydratedSpecs: SpecReview<{}>[]) { this.ensureMd(); //loop through all the plugins and render them diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index 0ad6ac8d..9e3f8965 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -5,7 +5,7 @@ let renderer: IDocs.markdown.Renderer; document.addEventListener('DOMContentLoaded', () => { let transactionIndex = 0; - const transactions: Record[]> = {}; + const transactions: Record[]> = {}; renderer = new IDocs.markdown.Renderer(document.body, { errorHandler: (error: Error, pluginName: string, instanceIndex: number, phase: string, container: Element, detail?: string) => { diff --git a/packages/sandbox/src/preview.ts b/packages/sandbox/src/preview.ts index 373b7617..e2ee0250 100644 --- a/packages/sandbox/src/preview.ts +++ b/packages/sandbox/src/preview.ts @@ -1,9 +1,9 @@ -import { Flagged, SandboxedPreHydrateMessage } from "common"; +import { SpecReview, SandboxedPreHydrateMessage } from "common"; export interface PreviewerOptions { onReady?: () => void; onError?: (error: Error) => void; - onApprove: (message: SandboxedPreHydrateMessage) => Flagged<{}>[]; + onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; } // Previewer class From e19ca082b8e8e73d4b6d0c5e36e14c909f55b0ba Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 05:26:20 -0700 Subject: [PATCH 15/20] rename to specs --- docs/view/view.js | 6 +++--- packages/common/src/messages.ts | 4 ++-- packages/sandbox-resources/src/sandboxed.ts | 6 +++--- packages/sandbox/dev/index.ts | 5 +++-- packages/sandbox/src/sandbox.ts | 4 ++-- packages/vscode-resources/src/edit.ts | 4 ++-- packages/vscode-resources/src/html-json.ts | 4 ++-- packages/vscode-resources/src/html-markdown.ts | 4 ++-- packages/vscode-resources/src/preview.ts | 4 ++-- packages/vscode-resources/src/view.ts | 6 +++--- 10 files changed, 24 insertions(+), 23 deletions(-) diff --git a/docs/view/view.js b/docs/view/view.js index 90826f87..19385918 100644 --- a/docs/view/view.js +++ b/docs/view/view.js @@ -7,9 +7,9 @@ window.addEventListener('DOMContentLoaded', () => { fileInput: '#file-input', textarea: '#textarea', onApprove: (message) => { - // TODO look through each and override policy to approve unapproved - const remediated = message.flags; - return remediated; + // TODO look through each spec and override policy to approve unapproved for https://microsoft.github.io/chartifact/ + const { specs } = message; + return specs; }, }); }); diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index afb87e5f..56676e35 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -16,13 +16,13 @@ export interface SpecReview { export interface SandboxedPreHydrateMessage { type: 'sandboxedPreHydrate'; transactionId: number; - flags: SpecReview<{}>[]; + specs: SpecReview<{}>[]; } export type SandboxApprovalMessage = { type: 'sandboxApproval'; transactionId: number; - remediated: SpecReview<{}>[]; + specs: SpecReview<{}>[]; }; export interface HostRenderRequestMessage { diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index 9e3f8965..72a5ba30 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -25,17 +25,17 @@ document.addEventListener('DOMContentLoaded', () => { const html = renderer.renderHtml(request.markdown); renderer.element.innerHTML = html; - const flags = renderer.hydrateSpecs(); + const specs = renderer.hydrateSpecs(); //todo: get stuff here for whitelist const transactionId = transactionIndex++; - transactions[transactionId] = flags; + transactions[transactionId] = specs; //send message to parent to ask for whitelist const sandboxedPreRenderMessage: IDocs.common.SandboxedPreHydrateMessage = { type: 'sandboxedPreHydrate', transactionId, - flags, + specs, }; window.parent.postMessage(sandboxedPreRenderMessage, '*'); } diff --git a/packages/sandbox/dev/index.ts b/packages/sandbox/dev/index.ts index 7e0676ad..b1d777c3 100644 --- a/packages/sandbox/dev/index.ts +++ b/packages/sandbox/dev/index.ts @@ -9,8 +9,9 @@ const sandbox = new Sandbox(document.body, textarea.value, { }, onApprove: (message) => { console.log('Sandbox approval message:', message); - //TODO policy to approve unapproved - return message.flags; + //TODO policy to approve unapproved on localhost + const { specs } = message; + return specs; } }); diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index c91708c6..4cc52c58 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -33,11 +33,11 @@ export class Sandbox extends Previewer { if (event.source === this.iframe.contentWindow) { const message = event.data as SandboxedPreHydrateMessage; if (message.type == 'sandboxedPreHydrate') { - const remediated = this.options.onApprove(message); + const specs = this.options.onApprove(message); const sandboxedApprovalMessage: SandboxApprovalMessage = { type: 'sandboxApproval', transactionId: message.transactionId, - remediated, + specs, }; this.iframe.contentWindow?.postMessage(sandboxedApprovalMessage, '*'); } diff --git a/packages/vscode-resources/src/edit.ts b/packages/vscode-resources/src/edit.ts index cd39b816..e0ca4538 100644 --- a/packages/vscode-resources/src/edit.ts +++ b/packages/vscode-resources/src/edit.ts @@ -25,8 +25,8 @@ window.addEventListener('DOMContentLoaded', () => { // Handle sandboxed pre-render message console.log('Handling sandboxed pre-render message:', message); //Here you can approve unapproved specs per your own policy - const remediated = message.flags; - return remediated; + const { specs } = message; + return specs; } }; const root = ReactDOM.createRoot(document.getElementById("app")); diff --git a/packages/vscode-resources/src/html-json.ts b/packages/vscode-resources/src/html-json.ts index ff7ce69d..b27290e8 100644 --- a/packages/vscode-resources/src/html-json.ts +++ b/packages/vscode-resources/src/html-json.ts @@ -20,8 +20,8 @@ window.addEventListener('DOMContentLoaded', () => { // Handle sandboxed pre-render message console.log('Handling sandboxed pre-render message:', message); //Here you can approve unapproved specs per your own policy - const remediated = message.flags; - return remediated; + const { specs } = message; + return specs; }, onError: (error) => { console.error('Sandbox error:', error); diff --git a/packages/vscode-resources/src/html-markdown.ts b/packages/vscode-resources/src/html-markdown.ts index 38a6d73a..25fec01c 100644 --- a/packages/vscode-resources/src/html-markdown.ts +++ b/packages/vscode-resources/src/html-markdown.ts @@ -5,8 +5,8 @@ window.addEventListener('DOMContentLoaded', () => { // Handle sandboxed pre-render message console.log('Handling sandboxed pre-render message:', message); //Here you can approve unapproved specs per your own policy - const remediated = message.flags; - return remediated; + const { specs } = message; + return specs; }, onError: (error) => { console.error('Sandbox error:', error); diff --git a/packages/vscode-resources/src/preview.ts b/packages/vscode-resources/src/preview.ts index 1a089490..4e5b96c9 100644 --- a/packages/vscode-resources/src/preview.ts +++ b/packages/vscode-resources/src/preview.ts @@ -11,8 +11,8 @@ window.addEventListener('DOMContentLoaded', () => { console.log('Handling sandboxed pre-render message:', message); // TODO look through each and override policy to approve unapproved - const remediated = message.flags; - return remediated; + const { specs } = message; + return specs; }, }); }); diff --git a/packages/vscode-resources/src/view.ts b/packages/vscode-resources/src/view.ts index a5849af0..3e38abe4 100644 --- a/packages/vscode-resources/src/view.ts +++ b/packages/vscode-resources/src/view.ts @@ -9,9 +9,9 @@ window.addEventListener('DOMContentLoaded', () => { fileInput: '#file-input', textarea: '#textarea', onApprove: (message: IDocs.common.SandboxedPreHydrateMessage) => { - // TODO look through each and override policy to approve unapproved - const remediated = message.flags; - return remediated; + // TODO look through each spec and override policy to approve unapproved for https://microsoft.github.io/chartifact/ + const { specs } = message; + return specs; }, }); }); From 261a6abe279c59390e76793827a390fec32bf836 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 05:27:47 -0700 Subject: [PATCH 16/20] rename to specs --- packages/editor/dev/previewer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/dev/previewer.ts b/packages/editor/dev/previewer.ts index e6c187b0..c563c43a 100644 --- a/packages/editor/dev/previewer.ts +++ b/packages/editor/dev/previewer.ts @@ -34,8 +34,8 @@ export class DevPreviewer extends Previewer { ${html}`; this.renderer.element.innerHTML = newHtml; - const x = this.renderer.hydrateSpecs(); - this.renderer.hydrate(x); + const specs = this.renderer.hydrateSpecs(); + this.renderer.hydrate(specs); } send(markdown: string) { From 500b9c62ea4d723a3abb1d315cf22176ffdb40ca Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 05:30:07 -0700 Subject: [PATCH 17/20] rename to specs --- packages/markdown/src/renderer.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/markdown/src/renderer.ts b/packages/markdown/src/renderer.ts index e71684a7..40cb4ead 100644 --- a/packages/markdown/src/renderer.ts +++ b/packages/markdown/src/renderer.ts @@ -90,7 +90,7 @@ export class Renderer { hydrateSpecs() { this.ensureMd(); - const allFlagged: SpecReview<{}>[] = []; + const specs: SpecReview<{}>[] = []; //loop through all the plugins and hydrate their specs and flag them id needed this.signalBus.log('Renderer', 'hydrate specs'); @@ -98,14 +98,14 @@ export class Renderer { for (let i = 0; i < plugins.length; i++) { const plugin = plugins[i]; if (plugin.hydrateSpecs) { - allFlagged.push(...plugin.hydrateSpecs(this, this.options.errorHandler)); + specs.push(...plugin.hydrateSpecs(this, this.options.errorHandler)); } } - return allFlagged; + return specs; } - async hydrate(hydratedSpecs: SpecReview<{}>[]) { + async hydrate(specs: SpecReview<{}>[]) { this.ensureMd(); //loop through all the plugins and render them @@ -116,9 +116,9 @@ export class Renderer { const plugin = plugins[i]; if (plugin.hydrateComponent) { //get only those specs that match the plugin name - const specContainers = hydratedSpecs.filter(spec => spec.pluginName === plugin.name); + const specsForPlugin = specs.filter(spec => spec.pluginName === plugin.name); //make a new promise that returns IInstances but adds the plugin name - hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler, specContainers).then(instances => { + hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler, specsForPlugin).then(instances => { return { pluginName: plugin.name, instances, From ad088ff443d4374f24fa6d15a21536dbe8b4cd4b Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 05:35:45 -0700 Subject: [PATCH 18/20] rename to specs --- packages/markdown/src/renderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/markdown/src/renderer.ts b/packages/markdown/src/renderer.ts index 40cb4ead..b8de3ddc 100644 --- a/packages/markdown/src/renderer.ts +++ b/packages/markdown/src/renderer.ts @@ -68,9 +68,9 @@ export class Renderer { const html = this.renderHtml(markdown); this.element.innerHTML = html; - const hydratedSpecs = this.hydrateSpecs(); + const specs = this.hydrateSpecs(); - await this.hydrate(hydratedSpecs); + await this.hydrate(specs); } renderHtml(markdown: string) { From 750eeb8ff5d98352c756cf446a3cf10c3f8a1b77 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 05:37:55 -0700 Subject: [PATCH 19/20] remove comment --- packages/sandbox-resources/src/sandboxed.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index 72a5ba30..21030183 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -27,7 +27,6 @@ document.addEventListener('DOMContentLoaded', () => { renderer.element.innerHTML = html; const specs = renderer.hydrateSpecs(); - //todo: get stuff here for whitelist const transactionId = transactionIndex++; transactions[transactionId] = specs; From 6f8a12867cd84bc2f2f4073e640aaf211ab1a3bf Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 05:43:02 -0700 Subject: [PATCH 20/20] remove console.log, add policy comments --- packages/vscode-resources/src/edit.ts | 5 ++--- packages/vscode-resources/src/html-json.ts | 2 -- packages/vscode-resources/src/html-markdown.ts | 2 -- packages/vscode-resources/src/preview.ts | 4 +--- 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/vscode-resources/src/edit.ts b/packages/vscode-resources/src/edit.ts index e0ca4538..8159d533 100644 --- a/packages/vscode-resources/src/edit.ts +++ b/packages/vscode-resources/src/edit.ts @@ -22,9 +22,8 @@ window.addEventListener('DOMContentLoaded', () => { previewer: OfflineSandbox, postMessageTarget: vscode as any, onApprove: (message) => { - // Handle sandboxed pre-render message - console.log('Handling sandboxed pre-render message:', message); - //Here you can approve unapproved specs per your own policy + // TODO look through each and override policy to approve unapproved + // policy from vscode settings const { specs } = message; return specs; } diff --git a/packages/vscode-resources/src/html-json.ts b/packages/vscode-resources/src/html-json.ts index b27290e8..a27bedb0 100644 --- a/packages/vscode-resources/src/html-json.ts +++ b/packages/vscode-resources/src/html-json.ts @@ -17,8 +17,6 @@ window.addEventListener('DOMContentLoaded', () => { if (!sandbox) { sandbox = new IDocs.sandbox.Sandbox('main', markdown, { onApprove: (message) => { - // Handle sandboxed pre-render message - console.log('Handling sandboxed pre-render message:', message); //Here you can approve unapproved specs per your own policy const { specs } = message; return specs; diff --git a/packages/vscode-resources/src/html-markdown.ts b/packages/vscode-resources/src/html-markdown.ts index 25fec01c..7069bc50 100644 --- a/packages/vscode-resources/src/html-markdown.ts +++ b/packages/vscode-resources/src/html-markdown.ts @@ -2,8 +2,6 @@ window.addEventListener('DOMContentLoaded', () => { const textarea = document.getElementById('markdown-input') as HTMLTextAreaElement; const sandbox = new IDocs.sandbox.Sandbox('main', textarea.value, { onApprove: (message) => { - // Handle sandboxed pre-render message - console.log('Handling sandboxed pre-render message:', message); //Here you can approve unapproved specs per your own policy const { specs } = message; return specs; diff --git a/packages/vscode-resources/src/preview.ts b/packages/vscode-resources/src/preview.ts index 4e5b96c9..cb3e509d 100644 --- a/packages/vscode-resources/src/preview.ts +++ b/packages/vscode-resources/src/preview.ts @@ -7,10 +7,8 @@ window.addEventListener('DOMContentLoaded', () => { loading: '#loading', options, onApprove: (message: IDocs.common.SandboxedPreHydrateMessage) => { - // Handle sandboxed pre-render message - console.log('Handling sandboxed pre-render message:', message); - // TODO look through each and override policy to approve unapproved + // policy from vscode settings const { specs } = message; return specs; },