From 62e3f4569ec0d3ecadd2ff64289885d1d39d996f Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 12:30:00 -0700 Subject: [PATCH 01/61] set package.json to private --- packages/common/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/common/package.json b/packages/common/package.json index 77dca624..4ac5b872 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,5 +1,6 @@ { "name": "common", + "private": true, "version": "1.0.0", "type": "module", "main": "dist/esnext/index.js", From 10ee443a1db2854445651bf152851ad190f6716c Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 12:30:34 -0700 Subject: [PATCH 02/61] add RendererCommonOptions and RenderRequestMessage interfaces --- packages/common/src/common.ts | 14 ++++++++++++++ packages/common/src/index.ts | 13 ++----------- packages/common/src/messages.ts | 3 +++ 3 files changed, 19 insertions(+), 11 deletions(-) create mode 100644 packages/common/src/common.ts create mode 100644 packages/common/src/messages.ts diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts new file mode 100644 index 00000000..78db993f --- /dev/null +++ b/packages/common/src/common.ts @@ -0,0 +1,14 @@ +export interface RendererCommonOptions { + dataNameSelectedSuffix?: string; + + /* dataSignalPrefix will set `isData` to true in the Signal Bus */ + dataSignalPrefix?: string; + + groupClassName?: string; +} + +export const defaultCommonOptions: RendererCommonOptions = { + dataNameSelectedSuffix: '_selected', + dataSignalPrefix: 'data_signal:', + groupClassName: 'group', +}; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index f40932d7..4bb11d8c 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,11 +1,2 @@ -export interface RendererCommonOptions { - dataNameSelectedSuffix?: string; - dataSignalPrefix?: string; - groupClassName?: string; -} - -export const defaultCommonOptions: RendererCommonOptions = { - dataNameSelectedSuffix: '_selected', - dataSignalPrefix: 'data_signal:', - groupClassName: 'group', -}; +export * from './common.js'; +export * from './messages.js'; diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts new file mode 100644 index 00000000..2efa4c92 --- /dev/null +++ b/packages/common/src/messages.ts @@ -0,0 +1,3 @@ +export interface RenderRequestMessage { + markdown?: string; +} From 362d534f36727df193cf86aa9caebb7366d4ce61 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 12:30:47 -0700 Subject: [PATCH 03/61] refactor exports in index.ts for improved clarity and organization --- packages/markdown/src/index.ts | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/markdown/src/index.ts b/packages/markdown/src/index.ts index d29a1be5..51ec94f1 100644 --- a/packages/markdown/src/index.ts +++ b/packages/markdown/src/index.ts @@ -3,23 +3,15 @@ * Licensed under the MIT License. */ -import { definePlugin, Plugin, plugins, registerMarkdownPlugin } from './factory.js'; -import { Renderer, RendererOptions } from './renderer.js'; import { registerNativePlugins } from './plugins/index.js'; -import { sanitizedHTML } from './sanitize.js'; registerNativePlugins(); -export { - definePlugin, - type Plugin, - plugins, - registerMarkdownPlugin, - Renderer, - type RendererOptions, - sanitizedHTML, -}; +export { sanitizedHTML } from './sanitize.js'; +export { definePlugin, plugins, registerMarkdownPlugin } from './factory.js'; +export { Renderer } from './renderer.js'; +export type { RendererOptions } from './renderer.js'; +export type { Plugin } from './factory.js'; export type * as Plugins from './plugins/interfaces.js'; export type { Batch, IInstance } from './factory.js'; -export * from './types.js'; From 54b06ba559648d061919e18e50a5a41b0b34c801 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 12:31:18 -0700 Subject: [PATCH 04/61] refactor: update RenderRequestMessage usage to import from common --- packages/markdown/src/types.ts | 3 --- packages/sandbox-resources/src/idocs.d.ts | 3 ++- packages/sandbox-resources/src/sandboxed.ts | 4 ++-- packages/sandbox/src/sandbox.ts | 6 +++--- 4 files changed, 7 insertions(+), 9 deletions(-) delete mode 100644 packages/markdown/src/types.ts diff --git a/packages/markdown/src/types.ts b/packages/markdown/src/types.ts deleted file mode 100644 index 2efa4c92..00000000 --- a/packages/markdown/src/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface RenderRequestMessage { - markdown?: string; -} diff --git a/packages/sandbox-resources/src/idocs.d.ts b/packages/sandbox-resources/src/idocs.d.ts index eed6c889..7204096a 100644 --- a/packages/sandbox-resources/src/idocs.d.ts +++ b/packages/sandbox-resources/src/idocs.d.ts @@ -1,5 +1,6 @@ import * as markdown from '@microsoft/interactive-document-markdown'; +import * as common from 'common'; export as namespace IDocs; -export { markdown }; +export { common, markdown }; diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index 11f7768c..447ab5c1 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -1,4 +1,4 @@ -declare const renderRequest: IDocs.markdown.RenderRequestMessage; +declare const renderRequest: IDocs.common.RenderRequestMessage; document.addEventListener('DOMContentLoaded', () => { const renderer = new IDocs.markdown.Renderer(document.body, { @@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => { } }); - function render(request: IDocs.markdown.RenderRequestMessage) { + function render(request: IDocs.common.RenderRequestMessage) { if (request.markdown) { renderer.reset(); const html = renderer.renderHtml(request.markdown); diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index fcfa0334..f58d66b6 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 { RenderRequestMessage } from '@microsoft/interactive-document-markdown'; +import type { RenderRequestMessage } from 'common'; export class Sandbox extends Previewer { private iframe: HTMLIFrameElement; @@ -34,8 +34,8 @@ export class Sandbox extends Previewer { } send(markdown: string): void { - //TODO get html and ensure it is sanitized - this.iframe.contentWindow?.postMessage({ markdown }, '*'); + const message: RenderRequestMessage = { markdown }; + this.iframe.contentWindow?.postMessage(message, '*'); } getDependencies() { From db765d80ea6dcb2296486ddbdba8b1a333ca1c89 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 12:31:26 -0700 Subject: [PATCH 05/61] refactor: clarify comment on legacy dataPrefix handling in createSpecInit function --- packages/markdown/src/plugins/vega.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index a7738b0b..771399bc 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -271,7 +271,7 @@ async function createSpecInit(container: Element, index: number, renderer: Rende const initialSignals: PrioritizedSignal[] = spec.signals?.map((signal: InitSignal | NewSignal) => { if (ignoredSignals.includes(signal.name)) return; let isData = isSignalDataBridge(signal as NewSignal); - //support legacy dataPrefix + //dataSignalPrefix will set isData to true if (signal.name.startsWith(defaultCommonOptions.dataSignalPrefix)) { isData = true; } From 813359db714221a0f07eb0ce8e3bd5c6d357b13f Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 14:59:13 -0700 Subject: [PATCH 06/61] bump build order --- package.json | 2 +- packages/common/package.json | 2 +- packages/compiler/package.json | 2 +- packages/editor/package.json | 2 +- packages/host/package.json | 2 +- packages/markdown/package.json | 2 +- packages/sandbox-resources/package.json | 2 +- packages/sandbox/package.json | 2 +- packages/vscode-resources/package.json | 2 +- packages/vscode/package.json | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index ed054142..3bcd52dc 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "1.0.0", "scripts": { "clean": "npm run clean --workspaces --if-present", - "build": "npm run build:00 --workspaces --if-present && npm run build:01 --workspaces --if-present && npm run build:02 --workspaces --if-present && npm run build:03 --workspaces --if-present && npm run build:04 --workspaces --if-present && npm run build:05 --workspaces --if-present && npm run build:06 --workspaces --if-present && npm run build:07 --workspaces --if-present", + "build": "npm run build:00 --workspaces --if-present && npm run build:01 --workspaces --if-present && npm run build:02 --workspaces --if-present && npm run build:03 --workspaces --if-present && npm run build:04 --workspaces --if-present && npm run build:05 --workspaces --if-present && npm run build:06 --workspaces --if-present && npm run build:07 --workspaces --if-present && npm run build:08 --workspaces --if-present", "start": "concurrently \"npm run dev -w common\" \"npm run dev -w @microsoft/interactive-document-markdown\" \"npm run dev -w schema\" \"npm run dev -w @microsoft/interactive-document-compiler\" \"npm run dev -w editor\"", "package": "npm run package --workspaces --if-present" }, diff --git a/packages/common/package.json b/packages/common/package.json index 4ac5b872..5a733ad9 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -9,7 +9,7 @@ "dev": "tsc -p . -w", "tsc": "tsc -p .", "build": "npm run tsc", - "build:00": "npm run build" + "build:01": "npm run build" }, "author": "Dan Marshall", "license": "MIT", diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 40e047d7..9e17ceaa 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -10,7 +10,7 @@ "tsc": "tsc -p .", "bundle": "vite build --config vite.bundle.config.js", "build": "npm run tsc && npm run bundle", - "build:02": "npm run build" + "build:03": "npm run build" }, "repository": { "type": "git", diff --git a/packages/editor/package.json b/packages/editor/package.json index 63ae0ca6..5bc5265f 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -10,7 +10,7 @@ "tsc": "tsc -p .", "bundle": "vite build --config vite.bundle.config.js", "build": "npm run tsc && npm run bundle", - "build:05": "npm run build" + "build:06": "npm run build" }, "author": "Dan Marshall", "license": "MIT", diff --git a/packages/host/package.json b/packages/host/package.json index 6ae741d3..48cc2fa6 100644 --- a/packages/host/package.json +++ b/packages/host/package.json @@ -13,7 +13,7 @@ "tsc": "tsc -p .", "bundle": "vite build --config vite.bundle.config.js", "build": "npm run tsc && npm run bundle", - "build:04": "npm run build", + "build:05": "npm run build", "preview": "vite preview", "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/packages/markdown/package.json b/packages/markdown/package.json index 390f6632..3d19eca0 100644 --- a/packages/markdown/package.json +++ b/packages/markdown/package.json @@ -18,7 +18,7 @@ "tsc": "tsc -p .", "bundle": "vite build --config vite.bundle.config.js", "build": "npm run tsc && npm run bundle", - "build:01": "npm run build" + "build:02": "npm run build" }, "repository": { "type": "git", diff --git a/packages/sandbox-resources/package.json b/packages/sandbox-resources/package.json index b9cbeb58..8bc7b842 100644 --- a/packages/sandbox-resources/package.json +++ b/packages/sandbox-resources/package.json @@ -6,7 +6,7 @@ "clean": "rimraf dist", "tsc": "tsc -p .", "build": "npm run tsc", - "build:02": "npm run build" + "build:03": "npm run build" }, "author": "Dan Marshall", "license": "MIT", diff --git a/packages/sandbox/package.json b/packages/sandbox/package.json index 9aaea300..cd76a051 100644 --- a/packages/sandbox/package.json +++ b/packages/sandbox/package.json @@ -12,7 +12,7 @@ "tsc": "tsc -p .", "bundle": "vite build --config vite.bundle.config.js", "build": "npm run tsc && npm run bundle", - "build:03": "npm run build", + "build:04": "npm run build", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Dan Marshall", diff --git a/packages/vscode-resources/package.json b/packages/vscode-resources/package.json index 8ccb611c..ef07672f 100644 --- a/packages/vscode-resources/package.json +++ b/packages/vscode-resources/package.json @@ -6,7 +6,7 @@ "scripts": { "clean": "rimraf dist", "tsc": "tsc -p .", - "build:06": "npm run tsc" + "build:07": "npm run tsc" }, "author": "Dan Marshall", "license": "MIT", diff --git a/packages/vscode/package.json b/packages/vscode/package.json index ba5e01c4..2ec0e14b 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -163,7 +163,7 @@ "pretest": "npm run compile-web", "vscode:prepublish": "npm run package-web", "build": "npm run resources && npm run compile-web", - "build:07": "npm run build", + "build:08": "npm run build", "compile-web": "npm run check-types && npm run lint && npm run resources && node esbuild.js", "watch-web": "npm-run-all -p watch-web:*", "watch-web:esbuild": "node esbuild.js --watch", From 49d567a915105634c63e1c80fcfd91ab7026503c Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 15:30:23 -0700 Subject: [PATCH 07/61] add host-test package --- package-lock.json | 8 +++++ packages/host-test/package.json | 14 +++++++++ packages/host-test/postmessage.html | 16 ++++++++++ packages/host-test/src/idocs.d.ts | 9 ++++++ .../host-test/src/index.ts | 30 ++++++------------- packages/host-test/tsconfig.json | 18 +++++++++++ 6 files changed, 74 insertions(+), 21 deletions(-) create mode 100644 packages/host-test/package.json create mode 100644 packages/host-test/postmessage.html create mode 100644 packages/host-test/src/idocs.d.ts rename docs/view/postmessage.html => packages/host-test/src/index.ts (66%) create mode 100644 packages/host-test/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 8f86f5fc..bec41319 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7605,6 +7605,10 @@ "he": "bin/he" } }, + "node_modules/host-test": { + "resolved": "packages/host-test", + "link": true + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -13189,6 +13193,10 @@ "version": "1.0.0", "license": "MIT" }, + "packages/host-test": { + "version": "1.0.0", + "license": "MIT" + }, "packages/markdown": { "name": "@microsoft/interactive-document-markdown", "version": "1.0.0", diff --git a/packages/host-test/package.json b/packages/host-test/package.json new file mode 100644 index 00000000..c3bac612 --- /dev/null +++ b/packages/host-test/package.json @@ -0,0 +1,14 @@ +{ + "name": "host-test", + "private": true, + "version": "1.0.0", + "scripts": { + "clean": "rimraf dist", + "tsc": "tsc -p .", + "build": "npm run tsc", + "build:06": "npm run build" + }, + "author": "Dan Marshall", + "license": "MIT", + "description": "" +} diff --git a/packages/host-test/postmessage.html b/packages/host-test/postmessage.html new file mode 100644 index 00000000..2064fd8c --- /dev/null +++ b/packages/host-test/postmessage.html @@ -0,0 +1,16 @@ + + + + + Minimal Parent + + +

Host Test

+

This page is used to test the host's postMessage functionality.

+

It must be run with a web server, the iframe cannot be loaded via the file:// protocol.

+ + + + + + diff --git a/packages/host-test/src/idocs.d.ts b/packages/host-test/src/idocs.d.ts new file mode 100644 index 00000000..09b43f62 --- /dev/null +++ b/packages/host-test/src/idocs.d.ts @@ -0,0 +1,9 @@ +import * as common from 'common'; +import * as compiler from '@microsoft/interactive-document-compiler'; +import * as host from '@microsoft/interactive-document-host'; +import * as sandbox from '@microsoft/chartifact-sandbox'; +import type * as schema from 'schema'; + +export as namespace IDocs; + +export { common, compiler, host, sandbox, schema }; diff --git a/docs/view/postmessage.html b/packages/host-test/src/index.ts similarity index 66% rename from docs/view/postmessage.html rename to packages/host-test/src/index.ts index b4659f88..954747bb 100644 --- a/docs/view/postmessage.html +++ b/packages/host-test/src/index.ts @@ -1,19 +1,10 @@ - - - - - Minimal Parent - - - +const iframe = document.getElementById('host') as HTMLIFrameElement; - - - + }, '*'); + } +}); \ No newline at end of file diff --git a/packages/host-test/tsconfig.json b/packages/host-test/tsconfig.json new file mode 100644 index 00000000..af8a78e9 --- /dev/null +++ b/packages/host-test/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "dist", + "module": "commonjs", + "typeRoots": [ + "./src" + ], + "skipLibCheck": true, + "target": "esnext", + "lib": [ + "dom", + "esnext" + ] + }, + "include": [ + "src" + ] +} From 26dc22725490ae82275f21257fc2c2eb8a89ee71 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 15:32:20 -0700 Subject: [PATCH 08/61] rename Host message types --- packages/common/src/messages.ts | 14 +++++++++++++- packages/host/src/listener.ts | 10 +++++----- packages/host/src/post-receive.ts | 4 ++-- packages/host/src/post-send.ts | 4 ++-- packages/host/src/types.ts | 13 ------------- packages/sandbox-resources/src/sandboxed.ts | 4 ++-- packages/sandbox/src/sandbox.ts | 8 ++++---- packages/vscode/src/web/command-preview.ts | 9 +++++---- 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index 2efa4c92..e4211305 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -1,3 +1,15 @@ -export interface RenderRequestMessage { +import { InteractiveDocument } from "schema"; + +export interface MarkdownRenderRequestMessage { markdown?: string; } + +export interface HostRenderRequestMessage extends MarkdownRenderRequestMessage { + interactiveDocument?: InteractiveDocument +} + +export interface HostStatusMessage { + hostStatus: 'ready' | 'compiling' | 'rendering' | 'rendered' | 'error' | 'loading'; + details?: string; + timestamp: number; +} diff --git a/packages/host/src/listener.ts b/packages/host/src/listener.ts index 5b271a2a..8d13c309 100644 --- a/packages/host/src/listener.ts +++ b/packages/host/src/listener.ts @@ -110,7 +110,7 @@ export class Listener { this.sandboxReady = true; // Send ready message to parent window (if embedded) - postStatus(this.options.postMessageTarget, { status: 'ready' }); + postStatus(this.options.postMessageTarget, { hostStatus: 'ready' }); }, onError: () => { this.errorHandler(new Error('Sandbox initialization failed'), 'Sandbox could not be initialized'); @@ -201,7 +201,7 @@ export class Listener { } private renderInteractiveDocument(content: InteractiveDocument) { - postStatus(this.options.postMessageTarget, { status: 'compiling', details: 'Starting interactive document compilation' }); + postStatus(this.options.postMessageTarget, { hostStatus: 'compiling', details: 'Starting interactive document compilation' }); const markdown = targetMarkdown(content); this.renderMarkdown(markdown); } @@ -215,18 +215,18 @@ export class Listener { this.hideLoadingAndHelp(); try { - postStatus(this.options.postMessageTarget, { status: 'rendering', details: 'Starting markdown rendering' }); + postStatus(this.options.postMessageTarget, { hostStatus: 'rendering', details: 'Starting markdown rendering' }); if (!this.sandbox || !this.sandboxReady) { this.createSandbox(markdown); } else { this.sandbox.send(markdown); } - postStatus(this.options.postMessageTarget, { status: 'rendered', details: 'Markdown rendering completed successfully' }); + postStatus(this.options.postMessageTarget, { hostStatus: 'rendered', details: 'Markdown rendering completed successfully' }); } catch (error) { this.errorHandler( error, 'Error rendering markdown content' ); - postStatus(this.options.postMessageTarget, { status: 'error', details: `Rendering failed: ${error.message}` }); + postStatus(this.options.postMessageTarget, { hostStatus: 'error', details: `Rendering failed: ${error.message}` }); } } diff --git a/packages/host/src/post-receive.ts b/packages/host/src/post-receive.ts index 58f215df..757ec6c1 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 { RenderRequestMessage } from './types.js'; +import type { HostRenderRequestMessage } from 'common'; export function setupPostMessageHandling(host: Listener) { window.addEventListener('message', (event) => { @@ -13,7 +13,7 @@ export function setupPostMessageHandling(host: Listener) { return; } - const data: RenderRequestMessage = event.data; + const data: HostRenderRequestMessage = event.data; if (data.markdown) { host.render(data.markdown, undefined); diff --git a/packages/host/src/post-send.ts b/packages/host/src/post-send.ts index 281090d7..36b62c90 100644 --- a/packages/host/src/post-send.ts +++ b/packages/host/src/post-send.ts @@ -1,6 +1,6 @@ -import type { StatusMessage } from './types.js'; +import type { HostStatusMessage } from 'common'; -export function postStatus(target: Window, message: Omit): void { +export function postStatus(target: Window, message: Omit): void { if (target) { const messageWithTimestamp = { ...message, diff --git a/packages/host/src/types.ts b/packages/host/src/types.ts index 08296b4c..f73fd75e 100644 --- a/packages/host/src/types.ts +++ b/packages/host/src/types.ts @@ -1,8 +1,6 @@ // Pure type definitions without DOM dependencies // This module can be safely imported by VS Code extensions -import { InteractiveDocument } from "schema"; - export interface ListenOptions { clipboard?: boolean; dragDrop?: boolean; @@ -12,14 +10,3 @@ export interface ListenOptions { url?: boolean; urlParamName?: string; } - -export interface RenderRequestMessage { - markdown?: string; - interactiveDocument?: InteractiveDocument -} - -export interface StatusMessage { - status: 'ready' | 'compiling' | 'rendering' | 'rendered' | 'error' | 'loading'; - details?: string; - timestamp: number; -} diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index 447ab5c1..0768770c 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -1,4 +1,4 @@ -declare const renderRequest: IDocs.common.RenderRequestMessage; +declare const renderRequest: IDocs.common.MarkdownRenderRequestMessage; document.addEventListener('DOMContentLoaded', () => { const renderer = new IDocs.markdown.Renderer(document.body, { @@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => { } }); - function render(request: IDocs.common.RenderRequestMessage) { + function render(request: IDocs.common.MarkdownRenderRequestMessage) { if (request.markdown) { renderer.reset(); const html = renderer.renderHtml(request.markdown); diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index f58d66b6..9162f9b8 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 { RenderRequestMessage } from 'common'; +import type { MarkdownRenderRequestMessage } from 'common'; export class Sandbox extends Previewer { private iframe: HTMLIFrameElement; @@ -10,7 +10,7 @@ export class Sandbox extends Previewer { constructor(elementOrSelector: string | HTMLElement, markdown: string, options?: PreviewerOptions) { super(elementOrSelector, markdown, options); - const renderRequest: RenderRequestMessage = { markdown }; + const renderRequest: MarkdownRenderRequestMessage = { markdown }; const { iframe } = createIframe(this.getDependencies(), renderRequest); this.iframe = iframe; @@ -34,7 +34,7 @@ export class Sandbox extends Previewer { } send(markdown: string): void { - const message: RenderRequestMessage = { markdown }; + const message: MarkdownRenderRequestMessage = { markdown }; this.iframe.contentWindow?.postMessage(message, '*'); } @@ -50,7 +50,7 @@ export class Sandbox extends Previewer { } } -function createIframe(dependencies: string, renderRequest: RenderRequestMessage) { +function createIframe(dependencies: string, renderRequest: MarkdownRenderRequestMessage) { const title = 'Interactive Document Sandbox'; const html = rendererHtml .replace('{{TITLE}}', () => title) diff --git a/packages/vscode/src/web/command-preview.ts b/packages/vscode/src/web/command-preview.ts index 0a6ca7ad..fc70fb4c 100644 --- a/packages/vscode/src/web/command-preview.ts +++ b/packages/vscode/src/web/command-preview.ts @@ -2,7 +2,8 @@ import * as vscode from 'vscode'; import { newPanel, WebViewWithUri } from './panel'; import { link, script } from './html'; import { getResourceContent } from './resources'; -import type { ListenOptions, RenderRequestMessage } from '@microsoft/interactive-document-host' with { 'resolution-mode': 'import' }; +import type { ListenOptions } from '@microsoft/interactive-document-host' with { 'resolution-mode': 'import' }; +import type { HostRenderRequestMessage, HostStatusMessage } from 'common' with { 'resolution-mode': 'import' }; /** * Manages the preview functionality for Interactive Documents @@ -69,8 +70,8 @@ export class PreviewManager { /** * Handles messages from the webview */ - private handleWebviewMessage(message: any, fileUri: vscode.Uri, uriFsPath: string) { - switch (message.status) { + private handleWebviewMessage(message: HostStatusMessage, fileUri: vscode.Uri, uriFsPath: string) { + switch (message.hostStatus) { case 'ready': { this.getFileContentAndRender(fileUri, uriFsPath); break; @@ -99,7 +100,7 @@ export class PreviewManager { }); } - public render(renderRequestMessage: RenderRequestMessage) { + public render(renderRequestMessage: HostRenderRequestMessage) { if (this.current && this.current.panel.visible) { this.current.panel.webview.postMessage(renderRequestMessage); } From 11bd4e504f9e00f8e000664ed7d8789331251933 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 17:51:52 -0700 Subject: [PATCH 09/61] SandboxApproval message types --- packages/common/src/messages.ts | 18 ++++++- packages/editor/dev/previewer.ts | 54 +++++++++++++++---- packages/editor/src/sandbox.tsx | 21 ++++++++ packages/host/src/post-receive.ts | 32 +++++++++--- packages/sandbox-resources/src/sandboxed.ts | 57 ++++++++++++++++++--- packages/sandbox/index.html | 16 ++++++ packages/sandbox/src/preview.ts | 6 +++ packages/sandbox/src/sandbox.ts | 18 +++++-- packages/vscode/src/web/command-preview.ts | 10 +++- 9 files changed, 202 insertions(+), 30 deletions(-) diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index e4211305..54f222f6 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -1,10 +1,26 @@ import { InteractiveDocument } from "schema"; export interface MarkdownRenderRequestMessage { + type: 'markdownRenderRequest'; markdown?: string; } -export interface HostRenderRequestMessage extends MarkdownRenderRequestMessage { +export interface SandboxedPreRenderMessage { + type: 'sandboxedPreRender'; + transactionId: number; + //todo put stuff here for whitelist +} + +export type SandboxApprovalMessage = { + type: 'sandboxApproval'; + transactionId: number; + approved: boolean; + //todo: add more fields as needed +}; + +export interface HostRenderRequestMessage { + type: 'hostRenderRequest'; + markdown?: string; interactiveDocument?: InteractiveDocument } diff --git a/packages/editor/dev/previewer.ts b/packages/editor/dev/previewer.ts index a39dd4b1..cb2c43a5 100644 --- a/packages/editor/dev/previewer.ts +++ b/packages/editor/dev/previewer.ts @@ -1,8 +1,12 @@ 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 { - private renderer: Renderer; + public renderer: Renderer; + public transactions: Record = {}; constructor(elementOrSelector: string | HTMLElement, markdown: string, options: PreviewerOptions) { super(elementOrSelector, markdown, options); @@ -30,14 +34,22 @@ export class DevPreviewer extends Previewer { render(markdown: string) { const html = this.renderer.renderHtml(markdown); - this.renderer.element.innerHTML = ` - - - ${html}`; - this.renderer.hydrate().catch(error => { - this.displayError('Failed to hydrate components'); - this.options.onError?.(error); - }); + + 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); + } send(markdown: string) { @@ -49,6 +61,30 @@ 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 6cd4f2dc..22a6b799 100644 --- a/packages/editor/src/sandbox.tsx +++ b/packages/editor/src/sandbox.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { InteractiveDocument } from "schema"; import { targetMarkdown } from '@microsoft/interactive-document-compiler'; import { Previewer, Sandbox } from '@microsoft/chartifact-sandbox'; +import { SandboxApprovalMessage, SandboxedPreRenderMessage } from 'common'; export interface SandboxDocumentPreviewProps { page: InteractiveDocument; @@ -13,6 +14,7 @@ export class SandboxDocumentPreview extends React.Component void; constructor(props: SandboxDocumentPreviewProps) { super(props); @@ -35,6 +37,24 @@ export class SandboxDocumentPreview extends React.Component { this.isSandboxReady = true; + this.windowMessageReceivedHandler = (event: MessageEvent) => { + const message = event.data as SandboxedPreRenderMessage; + if (message.type === 'sandboxedPreRender' && 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); @@ -79,6 +99,7 @@ export class SandboxDocumentPreview extends React.Component { @@ -13,14 +13,30 @@ export function setupPostMessageHandling(host: Listener) { return; } - const data: HostRenderRequestMessage = event.data; + const message = event.data as HostRenderRequestMessage | SandboxedPreRenderMessage; + if (message.type == 'hostRenderRequest') { + if (message.markdown) { + host.render(message.markdown, undefined); + } else if (message.interactiveDocument) { + host.render(undefined, message.interactiveDocument); + } else { + //do nothing, as messages may be directed to the page for other purposes + } + } else if (message.type == 'sandboxedPreRender') { + //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); - if (data.markdown) { - host.render(data.markdown, undefined); - } else if (data.interactiveDocument) { - host.render(undefined, data.interactiveDocument); - } else { - //do nothing, as messages may be directed to the page for other purposes + //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 0768770c..cd602973 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -1,6 +1,10 @@ declare const renderRequest: IDocs.common.MarkdownRenderRequestMessage; document.addEventListener('DOMContentLoaded', () => { + let transactionIndex = 0; + + const transactions: Record = {}; + const 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); @@ -15,11 +19,22 @@ document.addEventListener('DOMContentLoaded', () => { if (request.markdown) { renderer.reset(); const html = renderer.renderHtml(request.markdown); - //todo: look at dom elements prior to hydration - renderer.element.innerHTML = html; - //todo: send message to parent to ask for whitelist - //todo: asynchronously hydrate the renderer - renderer.hydrate(); + + //look at dom elements prior to hydration + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + + //todo: get stuff here for whitelist + const transactionId = transactionIndex++; + transactions[transactionId] = doc; + + //send message to parent to ask for whitelist + const sandboxedPreRenderMessage: IDocs.common.SandboxedPreRenderMessage = { + type: 'sandboxedPreRender', + transactionId, + //todo put stuff here for whitelist + }; + window.parent.postMessage(sandboxedPreRenderMessage, '*'); } } @@ -28,7 +43,37 @@ document.addEventListener('DOMContentLoaded', () => { //add listener for postMessage window.addEventListener('message', (event) => { if (!event.data) return; - render(event.data); + + const message = event.data as IDocs.common.SandboxApprovalMessage | IDocs.common.MarkdownRenderRequestMessage; + + switch (message.type) { + case 'markdownRenderRequest': { + render(message); + break; + } + case 'sandboxApproval': { + if (message.approved) { + + console.log('Sandbox approval received:', message.transactionId, transactionIndex); + + //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(); + } + } else { + console.warn('Received sandbox approval for an outdated transaction:', message.transactionId, transactionIndex); + } + } + break; + } + } }); }); \ No newline at end of file diff --git a/packages/sandbox/index.html b/packages/sandbox/index.html index a7ca41cd..21f65cd0 100644 --- a/packages/sandbox/index.html +++ b/packages/sandbox/index.html @@ -43,6 +43,22 @@ textarea.addEventListener('input', () => { sandbox.send(textarea.value); }); + window.addEventListener('message', (event) => { + if (!event.data) { + console.error('No data received from sandbox'); + return; + } + if (event.data.type === 'sandboxedPreRender') { + console.log('Sandbox pre-render received:', event.data); + //inspect doc and handle approval + const sandboxedApprovalMessage = { + type: 'sandboxApproval', + transactionId: event.data.transactionId, + approved: true // or false based on your logic + }; + sandbox.approve(sandboxedApprovalMessage); + } + }); diff --git a/packages/sandbox/src/preview.ts b/packages/sandbox/src/preview.ts index 7866a8e7..f86933b8 100644 --- a/packages/sandbox/src/preview.ts +++ b/packages/sandbox/src/preview.ts @@ -1,3 +1,5 @@ +import { SandboxApprovalMessage } from "common/dist/esnext/messages.js"; + export interface PreviewerOptions { onReady?: () => void; onError?: (error: Error) => void; @@ -23,4 +25,8 @@ 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 9162f9b8..b165555b 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -2,15 +2,18 @@ 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 { MarkdownRenderRequestMessage } from 'common'; +import type { MarkdownRenderRequestMessage, SandboxApprovalMessage } from 'common'; export class Sandbox extends Previewer { - private iframe: HTMLIFrameElement; + public iframe: HTMLIFrameElement; constructor(elementOrSelector: string | HTMLElement, markdown: string, options?: PreviewerOptions) { super(elementOrSelector, markdown, options); - const renderRequest: MarkdownRenderRequestMessage = { markdown }; + const renderRequest: MarkdownRenderRequestMessage = { + type: 'markdownRenderRequest', + markdown, + }; const { iframe } = createIframe(this.getDependencies(), renderRequest); this.iframe = iframe; @@ -34,7 +37,14 @@ export class Sandbox extends Previewer { } send(markdown: string): void { - const message: MarkdownRenderRequestMessage = { markdown }; + const message: MarkdownRenderRequestMessage = { + type: 'markdownRenderRequest', + markdown, + }; + this.iframe.contentWindow?.postMessage(message, '*'); + } + + approve(message: SandboxApprovalMessage) { this.iframe.contentWindow?.postMessage(message, '*'); } diff --git a/packages/vscode/src/web/command-preview.ts b/packages/vscode/src/web/command-preview.ts index fc70fb4c..4cca7458 100644 --- a/packages/vscode/src/web/command-preview.ts +++ b/packages/vscode/src/web/command-preview.ts @@ -85,13 +85,19 @@ export class PreviewManager { // If the file is a markdown file, we can send the markdown content if (uriFsPath.endsWith('.md')) { const markdown = new TextDecoder().decode(uint8array); - this.render({ markdown }); + this.render({ + type: 'hostRenderRequest', + markdown, + }); } else if (uriFsPath.endsWith('.json')) { // If the file is a JSON file, we can send the JSON content const jsonContent = new TextDecoder().decode(uint8array); try { const interactiveDocument = JSON.parse(jsonContent); - this.render({ interactiveDocument }); + this.render({ + type: 'hostRenderRequest', + interactiveDocument, + }); } catch (error) { vscode.window.showErrorMessage(`Failed to parse JSON: ${error}`); } From 72b938d0fe3533b4b58fdc0ff19b2df06d11a9f4 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 18:58:56 -0700 Subject: [PATCH 10/61] use preceding script tag --- packages/markdown/src/plugins/checkbox.ts | 11 ++++++----- packages/markdown/src/plugins/css.ts | 13 +++++++------ packages/markdown/src/plugins/dropdown.ts | 11 ++++++----- packages/markdown/src/plugins/image.ts | 11 ++++++----- packages/markdown/src/plugins/presets.ts | 11 ++++++----- packages/markdown/src/plugins/tabulator.ts | 11 ++++++----- packages/markdown/src/plugins/util.ts | 11 +++++++++++ packages/markdown/src/sanitize.ts | 18 +++++++++++++++--- 8 files changed, 63 insertions(+), 34 deletions(-) diff --git a/packages/markdown/src/plugins/checkbox.ts b/packages/markdown/src/plugins/checkbox.ts index d9a16b96..e1e7dddd 100644 --- a/packages/markdown/src/plugins/checkbox.ts +++ b/packages/markdown/src/plugins/checkbox.ts @@ -5,6 +5,7 @@ import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; +import { getJsonScriptTag } from './util.js'; interface CheckboxInstance { id: string; @@ -22,17 +23,17 @@ export const checkboxPlugin: Plugin = { name: 'checkbox', initializePlugin: (md) => definePlugin(md, 'checkbox'), fence: (token, idx) => { - const CheckboxId = `Checkbox-${idx}`; - return sanitizedHTML('div', { id: CheckboxId, class: 'checkbox' }, token.content.trim()); + return sanitizedHTML('div', { class: 'checkbox' }, token.content.trim(), true); }, hydrateComponent: async (renderer, errorHandler) => { const checkboxInstances: CheckboxInstance[] = []; const containers = renderer.element.querySelectorAll('.checkbox'); for (const [index, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; + const scriptTag = getJsonScriptTag(container); + if (!scriptTag) continue; try { - const spec: CheckboxSpec = JSON.parse(container.textContent); + const spec: CheckboxSpec = JSON.parse(scriptTag.textContent); const html = `
@@ -45,7 +46,7 @@ export const checkboxPlugin: Plugin = { container.innerHTML = html; const element = container.querySelector('input[type="checkbox"]') as HTMLInputElement; - const checkboxInstance: CheckboxInstance = { id: container.id, spec, element }; + const checkboxInstance: CheckboxInstance = { id: `checkbox-${index}`, spec, element }; checkboxInstances.push(checkboxInstance); } catch (e) { container.innerHTML = `
${e.toString()}
`; diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index 660a8f2b..37cbc457 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -6,6 +6,7 @@ import { definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import * as Csstree from 'css-tree'; +import { getJsonScriptTag } from './util.js'; // CSS Tree is expected to be available as a global variable declare const csstree: typeof Csstree; @@ -345,13 +346,12 @@ export const cssPlugin: Plugin = { const info = token.info.trim(); if (info === 'css') { - const cssId = `css-${idx}`; const cssContent = token.content.trim(); // Parse and categorize CSS content const categorizedCss = categorizeCss(cssContent); - return sanitizedHTML('div', { id: cssId, class: 'css-component' }, JSON.stringify(categorizedCss)); + return sanitizedHTML('div', { class: 'css-component' }, JSON.stringify(categorizedCss), true); } // Fallback to original fence renderer @@ -367,10 +367,11 @@ export const cssPlugin: Plugin = { const containers = renderer.element.querySelectorAll('.css-component'); for (const [index, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; + const scriptTag = getJsonScriptTag(container); + if (!scriptTag) continue; try { - const categorizedCss: CategorizedCss = JSON.parse(container.textContent); + const categorizedCss: CategorizedCss = JSON.parse(scriptTag.textContent); const comments: string[] = []; // Log security issues found @@ -385,7 +386,7 @@ export const cssPlugin: Plugin = { if (safeCss.trim().length > 0) { const styleElement = document.createElement('style'); styleElement.type = 'text/css'; - styleElement.id = `idocs-css-${container.id}`; + styleElement.id = `idocs-css-${index}`; styleElement.textContent = safeCss; // Apply to shadow DOM if available, otherwise document @@ -395,7 +396,7 @@ export const cssPlugin: Plugin = { comments.push(``); cssInstances.push({ - id: container.id, + id: `css-${index}`, element: styleElement }); } else { diff --git a/packages/markdown/src/plugins/dropdown.ts b/packages/markdown/src/plugins/dropdown.ts index a268347d..51cf6e56 100644 --- a/packages/markdown/src/plugins/dropdown.ts +++ b/packages/markdown/src/plugins/dropdown.ts @@ -5,6 +5,7 @@ import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; +import { getJsonScriptTag } from './util.js'; interface DropdownInstance { id: string; @@ -32,17 +33,17 @@ export const dropdownPlugin: Plugin = { name: 'dropdown', initializePlugin: (md) => definePlugin(md, 'dropdown'), fence: (token, idx) => { - const DropdownId = `Dropdown-${idx}`; - return sanitizedHTML('div', { id: DropdownId, class: 'dropdown' }, token.content.trim()); + return sanitizedHTML('div', { class: 'dropdown' }, token.content.trim(), true); }, hydrateComponent: async (renderer, errorHandler) => { const dropdownInstances: DropdownInstance[] = []; const containers = renderer.element.querySelectorAll('.dropdown'); for (const [index, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; + const scriptTag = getJsonScriptTag(container); + if (!scriptTag) continue; try { - const spec: DropdownSpec = JSON.parse(container.textContent); + const spec: DropdownSpec = JSON.parse(scriptTag.textContent); const html = `
@@ -59,7 +60,7 @@ export const dropdownPlugin: Plugin = { // Safely set the initial options setSelectOptions(element, spec.multiple ?? false, spec.options ?? [], spec.value ?? (spec.multiple ? [] : "")); - const dropdownInstance: DropdownInstance = { id: container.id, spec, element }; + const dropdownInstance: DropdownInstance = { id: `dropdown-${index}`, spec, element }; dropdownInstances.push(dropdownInstance); } catch (e) { container.innerHTML = `
${e.toString()}
`; diff --git a/packages/markdown/src/plugins/image.ts b/packages/markdown/src/plugins/image.ts index 50ed26ec..2dd677e0 100644 --- a/packages/markdown/src/plugins/image.ts +++ b/packages/markdown/src/plugins/image.ts @@ -5,6 +5,7 @@ import { definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; +import { getJsonScriptTag } from './util.js'; export interface ImageSpec { srcSignalName: string; @@ -30,16 +31,16 @@ export const imagePlugin: Plugin = { name: 'image', initializePlugin: (md) => definePlugin(md, 'image'), fence: (token, idx) => { - const ImageId = `Image-${idx}`; - return sanitizedHTML('div', { id: ImageId, class: 'image' }, token.content.trim()); + return sanitizedHTML('div', { class: 'image' }, token.content.trim(), true); }, hydrateComponent: async (renderer, errorHandler) => { const imageInstances: ImageInstance[] = []; const containers = renderer.element.querySelectorAll('.image'); for (const [index, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; + const scriptTag = getJsonScriptTag(container); + if (!scriptTag) continue; try { - const spec: ImageSpec = JSON.parse(container.textContent); + const spec: ImageSpec = JSON.parse(scriptTag.textContent); const element = document.createElement('img'); const spinner = document.createElement('div'); spinner.innerHTML = ` @@ -68,7 +69,7 @@ export const imagePlugin: Plugin = { container.appendChild(spinner); container.appendChild(element); - const imageInstance: ImageInstance = { id: container.id, spec, element, spinner }; + const imageInstance: ImageInstance = { id: `image-${index}`, spec, element, spinner }; imageInstances.push(imageInstance); } catch (e) { container.innerHTML = `
${e.toString()}
`; diff --git a/packages/markdown/src/plugins/presets.ts b/packages/markdown/src/plugins/presets.ts index dbbf6668..25fa1522 100644 --- a/packages/markdown/src/plugins/presets.ts +++ b/packages/markdown/src/plugins/presets.ts @@ -5,6 +5,7 @@ import { Batch, definePlugin, IInstance, Plugin, PrioritizedSignal } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; +import { getJsonScriptTag } from './util.js'; interface Preset { name: string; @@ -25,19 +26,19 @@ export const presetsPlugin: Plugin = { initializePlugin: (md) => definePlugin(md, 'presets'), fence: (token, idx) => { const spec = JSON.parse(token.content.trim()); - const pluginId = `preset-${idx}`; - return sanitizedHTML('div', { id: pluginId, class: 'presets' }, JSON.stringify(spec)); + return sanitizedHTML('div', { class: 'presets' }, JSON.stringify(spec), true); }, hydrateComponent: async (renderer, errorHandler) => { const presetsInstances: PresetsInstance[] = []; const containers = renderer.element.querySelectorAll('.presets'); for (const [index, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; + const scriptTag = getJsonScriptTag(container); + if (!scriptTag) continue; - const id = `presets${index}`; + const id = `presets-${index}`; let presets: Preset[]; try { - presets = JSON.parse(container.textContent) as Preset[]; + presets = JSON.parse(scriptTag.textContent) as Preset[]; } catch (e) { container.innerHTML = `
${e.toString()}
`; errorHandler(e, 'presets', index, 'parse', container); diff --git a/packages/markdown/src/plugins/tabulator.ts b/packages/markdown/src/plugins/tabulator.ts index 69972b06..9f633991 100644 --- a/packages/markdown/src/plugins/tabulator.ts +++ b/packages/markdown/src/plugins/tabulator.ts @@ -7,6 +7,7 @@ import { defaultCommonOptions } from 'common'; import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { Tabulator as TabulatorType, Options as TabulatorOptions } from 'tabulator-tables'; +import { getJsonScriptTag } from './util.js'; interface TabulatorInstance { id: string; @@ -26,21 +27,21 @@ export const tabulatorPlugin: Plugin = { name: 'tabulator', initializePlugin: (md) => definePlugin(md, 'tabulator'), fence: (token, idx) => { - const tabulatorId = `tabulator-${idx}`; - return sanitizedHTML('div', { id: tabulatorId, class: 'tabulator', style: 'box-sizing: border-box;' }, token.content.trim()); + return sanitizedHTML('div', { class: 'tabulator', style: 'box-sizing: border-box;' }, token.content.trim(), true); }, hydrateComponent: async (renderer, errorHandler) => { const tabulatorInstances: TabulatorInstance[] = []; const containers = renderer.element.querySelectorAll('.tabulator'); for (const [index, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; + const scriptTag = getJsonScriptTag(container); + if (!scriptTag) continue; if (!Tabulator) { errorHandler(new Error('Tabulator not found'), 'tabulator', index, 'init', container); continue; } try { - const spec: TabulatorSpec = JSON.parse(container.textContent); + const spec: TabulatorSpec = JSON.parse(scriptTag.textContent); let options: TabulatorOptions = { autoColumns: true, @@ -54,7 +55,7 @@ export const tabulatorPlugin: Plugin = { } const table = new Tabulator(container as HTMLElement, options); - const tabulatorInstance: TabulatorInstance = { id: container.id, spec, table, built: false }; + const tabulatorInstance: TabulatorInstance = { id: `tabulator-${index}`, spec, table, built: false }; table.on('tableBuilt', () => { table.off('tableBuilt'); tabulatorInstance.built = true; diff --git a/packages/markdown/src/plugins/util.ts b/packages/markdown/src/plugins/util.ts index a67ac240..31e2c537 100644 --- a/packages/markdown/src/plugins/util.ts +++ b/packages/markdown/src/plugins/util.ts @@ -6,3 +6,14 @@ export function urlParam(urlParamName: string, value: any) { return `${urlParamName}=${encodeURIComponent(value)}`; } } + +export function getJsonScriptTag(container: Element): HTMLScriptElement | null { + const scriptTag = container.previousElementSibling; + if (scriptTag?.tagName !== 'SCRIPT' || scriptTag.getAttribute('type') !== 'application/json') { + return null; + } + if (!scriptTag.textContent) { + return null; + } + return scriptTag as HTMLScriptElement; +} diff --git a/packages/markdown/src/sanitize.ts b/packages/markdown/src/sanitize.ts index 6499a926..42097c7a 100644 --- a/packages/markdown/src/sanitize.ts +++ b/packages/markdown/src/sanitize.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -export function sanitizedHTML(tagName: string, attributes: { [key: string]: string }, content: string) { +export function sanitizedHTML(tagName: string, attributes: { [key: string]: string }, content: string, precedeWithScriptTag?: boolean) { // Create a temp element with the specified tag name const element = document.createElement(tagName); @@ -13,8 +13,20 @@ export function sanitizedHTML(tagName: string, attributes: { [key: string]: stri element.setAttribute(key, attributes[key]); }); - // Set the textContent to automatically escape the content - element.textContent = content; + if (precedeWithScriptTag) { + // Create a script tag that precedes the main element + const scriptElement = document.createElement('script'); + scriptElement.setAttribute('type', 'application/json'); + // Only escape the dangerous sequence that could break out of script tag + const safeContent = content.replace(/<\/script>/gi, '<\\/script>'); + scriptElement.innerHTML = safeContent; + + // Return script tag followed by empty element + return scriptElement.outerHTML + element.outerHTML; + } else { + // Set the textContent to automatically escape the content + element.textContent = content; + } // Return the outer HTML of the element return element.outerHTML; From d2437037460d70ba453c2070726892eb0daa683b Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 21:03:53 -0700 Subject: [PATCH 11/61] rename message types --- packages/common/src/messages.ts | 8 ++++---- packages/editor/src/sandbox.tsx | 6 +++--- packages/host-test/postmessage.html | 2 +- packages/host-test/src/index.ts | 15 ++++++++------- packages/host/src/post-receive.ts | 6 +++--- packages/host/src/post-send.ts | 2 +- packages/sandbox-resources/src/sandboxed.ts | 12 ++++++------ packages/sandbox/index.html | 2 +- packages/sandbox/src/sandbox.ts | 12 ++++++------ 9 files changed, 33 insertions(+), 32 deletions(-) diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index 54f222f6..c9678032 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -1,12 +1,12 @@ import { InteractiveDocument } from "schema"; -export interface MarkdownRenderRequestMessage { - type: 'markdownRenderRequest'; +export interface SandboxRenderMessage { + type: 'sandboxRender'; markdown?: string; } -export interface SandboxedPreRenderMessage { - type: 'sandboxedPreRender'; +export interface SandboxedPreHydrateMessage { + type: 'sandboxedPreHydrate'; transactionId: number; //todo put stuff here for whitelist } diff --git a/packages/editor/src/sandbox.tsx b/packages/editor/src/sandbox.tsx index 22a6b799..9ec328a1 100644 --- a/packages/editor/src/sandbox.tsx +++ b/packages/editor/src/sandbox.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { InteractiveDocument } from "schema"; import { targetMarkdown } from '@microsoft/interactive-document-compiler'; import { Previewer, Sandbox } from '@microsoft/chartifact-sandbox'; -import { SandboxApprovalMessage, SandboxedPreRenderMessage } from 'common'; +import { SandboxApprovalMessage, SandboxedPreHydrateMessage } from 'common'; export interface SandboxDocumentPreviewProps { page: InteractiveDocument; @@ -38,8 +38,8 @@ export class SandboxDocumentPreview extends React.Component { - const message = event.data as SandboxedPreRenderMessage; - if (message.type === 'sandboxedPreRender' && this.sandboxRef) { + 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); diff --git a/packages/host-test/postmessage.html b/packages/host-test/postmessage.html index 2064fd8c..3463f50b 100644 --- a/packages/host-test/postmessage.html +++ b/packages/host-test/postmessage.html @@ -2,7 +2,7 @@ - Minimal Parent + Host postmessage test

Host Test

diff --git a/packages/host-test/src/index.ts b/packages/host-test/src/index.ts index 954747bb..c9c977b1 100644 --- a/packages/host-test/src/index.ts +++ b/packages/host-test/src/index.ts @@ -1,10 +1,11 @@ const iframe = document.getElementById('host') as HTMLIFrameElement; window.addEventListener('message', (event) => { - const hostStatusMessage = event.data as IDocs.common.HostStatusMessage; - if (hostStatusMessage.hostStatus === 'ready') { - iframe.contentWindow.postMessage({ - markdown: `# Auto-loaded Content + const hostStatusMessage = event.data as IDocs.common.HostStatusMessage; + if (hostStatusMessage.hostStatus === 'ready') { + const message: IDocs.common.HostRenderRequestMessage = { + type: 'hostRenderRequest', + markdown: `# Auto-loaded Content This markdown was automatically sent when the iframe became ready. @@ -48,7 +49,7 @@ The colors distinguish between different weather conditions such as sun, fog, dr } } \`\`\` -` - }, '*'); - } +`}; + iframe.contentWindow.postMessage(message, '*'); + } }); \ No newline at end of file diff --git a/packages/host/src/post-receive.ts b/packages/host/src/post-receive.ts index 5daf00c1..ce139910 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, SandboxedPreRenderMessage } from 'common'; +import type { HostRenderRequestMessage, SandboxApprovalMessage, SandboxedPreHydrateMessage } from 'common'; export function setupPostMessageHandling(host: Listener) { window.addEventListener('message', (event) => { @@ -13,7 +13,7 @@ export function setupPostMessageHandling(host: Listener) { return; } - const message = event.data as HostRenderRequestMessage | SandboxedPreRenderMessage; + const message = event.data as HostRenderRequestMessage | SandboxedPreHydrateMessage; if (message.type == 'hostRenderRequest') { if (message.markdown) { host.render(message.markdown, undefined); @@ -22,7 +22,7 @@ export function setupPostMessageHandling(host: Listener) { } else { //do nothing, as messages may be directed to the page for other purposes } - } else if (message.type == 'sandboxedPreRender') { + } 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 diff --git a/packages/host/src/post-send.ts b/packages/host/src/post-send.ts index 36b62c90..fb5c8759 100644 --- a/packages/host/src/post-send.ts +++ b/packages/host/src/post-send.ts @@ -2,7 +2,7 @@ import type { HostStatusMessage } from 'common'; export function postStatus(target: Window, message: Omit): void { if (target) { - const messageWithTimestamp = { + const messageWithTimestamp: HostStatusMessage = { ...message, timestamp: Date.now() }; diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index cd602973..2dd5e296 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -1,4 +1,4 @@ -declare const renderRequest: IDocs.common.MarkdownRenderRequestMessage; +declare const renderRequest: IDocs.common.SandboxRenderMessage; document.addEventListener('DOMContentLoaded', () => { let transactionIndex = 0; @@ -15,7 +15,7 @@ document.addEventListener('DOMContentLoaded', () => { } }); - function render(request: IDocs.common.MarkdownRenderRequestMessage) { + function render(request: IDocs.common.SandboxRenderMessage) { if (request.markdown) { renderer.reset(); const html = renderer.renderHtml(request.markdown); @@ -29,8 +29,8 @@ document.addEventListener('DOMContentLoaded', () => { transactions[transactionId] = doc; //send message to parent to ask for whitelist - const sandboxedPreRenderMessage: IDocs.common.SandboxedPreRenderMessage = { - type: 'sandboxedPreRender', + const sandboxedPreRenderMessage: IDocs.common.SandboxedPreHydrateMessage = { + type: 'sandboxedPreHydrate', transactionId, //todo put stuff here for whitelist }; @@ -44,10 +44,10 @@ document.addEventListener('DOMContentLoaded', () => { window.addEventListener('message', (event) => { if (!event.data) return; - const message = event.data as IDocs.common.SandboxApprovalMessage | IDocs.common.MarkdownRenderRequestMessage; + const message = event.data as IDocs.common.SandboxApprovalMessage | IDocs.common.SandboxRenderMessage; switch (message.type) { - case 'markdownRenderRequest': { + case 'sandboxRender': { render(message); break; } diff --git a/packages/sandbox/index.html b/packages/sandbox/index.html index 21f65cd0..cd78da82 100644 --- a/packages/sandbox/index.html +++ b/packages/sandbox/index.html @@ -48,7 +48,7 @@ console.error('No data received from sandbox'); return; } - if (event.data.type === 'sandboxedPreRender') { + if (event.data.type === 'sandboxedPreHydrate') { console.log('Sandbox pre-render received:', event.data); //inspect doc and handle approval const sandboxedApprovalMessage = { diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index b165555b..92faa4d5 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 { MarkdownRenderRequestMessage, SandboxApprovalMessage } from 'common'; +import type { SandboxRenderMessage, SandboxApprovalMessage } from 'common'; export class Sandbox extends Previewer { public iframe: HTMLIFrameElement; @@ -10,8 +10,8 @@ export class Sandbox extends Previewer { constructor(elementOrSelector: string | HTMLElement, markdown: string, options?: PreviewerOptions) { super(elementOrSelector, markdown, options); - const renderRequest: MarkdownRenderRequestMessage = { - type: 'markdownRenderRequest', + const renderRequest: SandboxRenderMessage = { + type: 'sandboxRender', markdown, }; @@ -37,8 +37,8 @@ export class Sandbox extends Previewer { } send(markdown: string): void { - const message: MarkdownRenderRequestMessage = { - type: 'markdownRenderRequest', + const message: SandboxRenderMessage = { + type: 'sandboxRender', markdown, }; this.iframe.contentWindow?.postMessage(message, '*'); @@ -60,7 +60,7 @@ export class Sandbox extends Previewer { } } -function createIframe(dependencies: string, renderRequest: MarkdownRenderRequestMessage) { +function createIframe(dependencies: string, renderRequest: SandboxRenderMessage) { const title = 'Interactive Document Sandbox'; const html = rendererHtml .replace('{{TITLE}}', () => title) From 0f06ff79e5ee8a61d1029d58b52e6d46dee2c01f Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 21:14:38 -0700 Subject: [PATCH 12/61] Add hostStatus type to HostStatusMessage and update related message handling --- packages/common/src/messages.ts | 2 +- packages/host-test/src/index.ts | 2 +- packages/host/src/post-send.ts | 9 ++------- packages/vscode/src/web/command-preview.ts | 10 ++++++---- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index c9678032..8a210f7b 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -25,7 +25,7 @@ export interface HostRenderRequestMessage { } export interface HostStatusMessage { + type: 'hostStatus'; hostStatus: 'ready' | 'compiling' | 'rendering' | 'rendered' | 'error' | 'loading'; details?: string; - timestamp: number; } diff --git a/packages/host-test/src/index.ts b/packages/host-test/src/index.ts index c9c977b1..f8f94e5c 100644 --- a/packages/host-test/src/index.ts +++ b/packages/host-test/src/index.ts @@ -2,7 +2,7 @@ const iframe = document.getElementById('host') as HTMLIFrameElement; window.addEventListener('message', (event) => { const hostStatusMessage = event.data as IDocs.common.HostStatusMessage; - if (hostStatusMessage.hostStatus === 'ready') { + if (hostStatusMessage.type === 'hostStatus' && hostStatusMessage.hostStatus === 'ready') { const message: IDocs.common.HostRenderRequestMessage = { type: 'hostRenderRequest', markdown: `# Auto-loaded Content diff --git a/packages/host/src/post-send.ts b/packages/host/src/post-send.ts index fb5c8759..2457bdea 100644 --- a/packages/host/src/post-send.ts +++ b/packages/host/src/post-send.ts @@ -1,12 +1,7 @@ import type { HostStatusMessage } from 'common'; -export function postStatus(target: Window, message: Omit): void { +export function postStatus(target: Window, message: HostStatusMessage): void { if (target) { - const messageWithTimestamp: HostStatusMessage = { - ...message, - timestamp: Date.now() - }; - - target.postMessage(messageWithTimestamp, '*'); + target.postMessage(message, '*'); } } diff --git a/packages/vscode/src/web/command-preview.ts b/packages/vscode/src/web/command-preview.ts index 4cca7458..181420a1 100644 --- a/packages/vscode/src/web/command-preview.ts +++ b/packages/vscode/src/web/command-preview.ts @@ -71,10 +71,12 @@ export class PreviewManager { * Handles messages from the webview */ private handleWebviewMessage(message: HostStatusMessage, fileUri: vscode.Uri, uriFsPath: string) { - switch (message.hostStatus) { - case 'ready': { - this.getFileContentAndRender(fileUri, uriFsPath); - break; + if (message.type === 'hostStatus') { + switch (message.hostStatus) { + case 'ready': { + this.getFileContentAndRender(fileUri, uriFsPath); + break; + } } } } From e71a339ad425126f23db8ad419ab73e019f7736d Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 21:40:33 -0700 Subject: [PATCH 13/61] move editor message types --- packages/common/src/messages.ts | 22 ++++++++++++++++++++++ packages/editor/src/app.tsx | 16 ++++++++-------- packages/editor/src/editor.tsx | 14 +++++++------- packages/editor/src/index.ts | 1 - packages/editor/src/sandbox.tsx | 2 +- packages/editor/src/types.ts | 16 ---------------- packages/host/src/listener.ts | 10 +++++----- packages/vscode-resources/src/edit.ts | 13 ++++++++++--- packages/vscode-resources/src/idocs.d.ts | 3 ++- packages/vscode/src/web/command-edit.ts | 24 +++++++++++++----------- 10 files changed, 68 insertions(+), 53 deletions(-) delete mode 100644 packages/editor/src/types.ts diff --git a/packages/common/src/messages.ts b/packages/common/src/messages.ts index 8a210f7b..3bbe49ed 100644 --- a/packages/common/src/messages.ts +++ b/packages/common/src/messages.ts @@ -29,3 +29,25 @@ export interface HostStatusMessage { hostStatus: 'ready' | 'compiling' | 'rendering' | 'rendered' | 'error' | 'loading'; details?: string; } + +export interface EditorReadyMessage { + type: 'editorReady'; + sender: string; +} + +export interface EditorPageMessage { + type: 'editorPage'; + sender: string; + page: InteractiveDocument; +} + +export interface EditorGetOfflineDependenciesMessage { + type: 'editorGetOfflineDependencies'; + sender: string; +} + +export interface EditorSetOfflineDependenciesMessage { + type: 'editorSetOfflineDependencies'; + sender: string; + offlineDeps: string; +} diff --git a/packages/editor/src/app.tsx b/packages/editor/src/app.tsx index f865d2b1..09b27e36 100644 --- a/packages/editor/src/app.tsx +++ b/packages/editor/src/app.tsx @@ -1,7 +1,7 @@ import { InteractiveDocument } from "schema"; import { Editor } from './editor.js'; -import { PageMessage, EditorMessage } from "./types.js"; import { Previewer } from '@microsoft/chartifact-sandbox'; +import { EditorPageMessage, EditorReadyMessage } from "common"; export interface AppProps { previewer: typeof Previewer; @@ -45,25 +45,25 @@ export function App(props: AppProps) { } // Post message to the editor within the same window - const pageMessage: PageMessage = { - type: 'page', + const pageMessage: EditorPageMessage = { + type: 'editorPage', page: page, - sender: 'app' + sender: 'app', }; window.postMessage(pageMessage, '*'); }; React.useEffect(() => { // Listen for messages from the editor - const handleMessage = (event: MessageEvent) => { + const handleMessage = (event: MessageEvent) => { // Only process messages from editor, ignore our own messages if (event.data && event.data.sender === 'editor') { - if (event.data.type === 'ready') { + if (event.data.type === 'editorReady') { setIsEditorReady(true); // Send initial page when editor is ready sendPageToEditor(currentPage); - } else if (event.data.type === 'page' && event.data.page) { - const pageMessage = event.data as PageMessage; + } 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 => { diff --git a/packages/editor/src/editor.tsx b/packages/editor/src/editor.tsx index 791d5dc0..1c42bf8b 100644 --- a/packages/editor/src/editor.tsx +++ b/packages/editor/src/editor.tsx @@ -1,7 +1,7 @@ import { InteractiveDocument } from "schema"; -import { EditorMessage, PageMessage, ReadyMessage } from "./types.js"; import { SandboxDocumentPreview } from "./sandbox.js"; import { Previewer } from '@microsoft/chartifact-sandbox'; +import { EditorPageMessage, EditorReadyMessage } from "common"; export interface EditorProps { postMessageTarget?: Window; @@ -35,13 +35,13 @@ export function Editor(props: EditorProps) { })); React.useEffect(() => { - const handleMessage = (event: MessageEvent) => { + const handleMessage = (event: MessageEvent) => { // Optionally add origin validation here for security // if (event.origin !== 'expected-origin') return; // Only process messages that are not from us (editor) if (event.data && event.data.sender !== 'editor') { - if (event.data.type === 'page' && event.data.page) { + if (event.data.type === 'editorPage' && event.data.page) { setPage(event.data.page); } } @@ -56,8 +56,8 @@ export function Editor(props: EditorProps) { React.useEffect(() => { // Send ready message when the editor is mounted and ready - const readyMessage: ReadyMessage = { - type: 'ready', + const readyMessage: EditorReadyMessage = { + type: 'editorReady', sender: 'editor' }; postMessageTarget.postMessage(readyMessage, '*'); @@ -76,8 +76,8 @@ export function EditorView(props: EditorViewProps) { const { page, postMessageTarget, previewer } = props; const sendEditToApp = (newPage: InteractiveDocument) => { - const pageMessage: PageMessage = { - type: 'page', + const pageMessage: EditorPageMessage = { + type: 'editorPage', page: newPage, sender: 'editor' }; diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index 941f28e2..d2dfdc30 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -1,4 +1,3 @@ export { Editor } from './editor.js'; export { App } from './app.js'; -export type * from './types.js'; export type { EditorProps } from './editor.js'; diff --git a/packages/editor/src/sandbox.tsx b/packages/editor/src/sandbox.tsx index 9ec328a1..5c793d59 100644 --- a/packages/editor/src/sandbox.tsx +++ b/packages/editor/src/sandbox.tsx @@ -37,7 +37,7 @@ export class SandboxDocumentPreview extends React.Component { this.isSandboxReady = true; - this.windowMessageReceivedHandler = (event: MessageEvent) => { + this.windowMessageReceivedHandler = (event: MessageEvent) => { const message = event.data as SandboxedPreHydrateMessage; if (message.type === 'sandboxedPreHydrate' && this.sandboxRef) { // Handle sandboxed pre-render message diff --git a/packages/editor/src/types.ts b/packages/editor/src/types.ts deleted file mode 100644 index 077bce95..00000000 --- a/packages/editor/src/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { InteractiveDocument } from "schema"; - -export interface BaseMessage { - sender: 'app' | 'editor'; -} - -export interface PageMessage extends BaseMessage { - type: 'page'; - page: InteractiveDocument; -} - -export interface ReadyMessage extends BaseMessage { - type: 'ready'; -} - -export type EditorMessage = PageMessage | ReadyMessage; diff --git a/packages/host/src/listener.ts b/packages/host/src/listener.ts index 8d13c309..30f307c1 100644 --- a/packages/host/src/listener.ts +++ b/packages/host/src/listener.ts @@ -110,7 +110,7 @@ export class Listener { this.sandboxReady = true; // Send ready message to parent window (if embedded) - postStatus(this.options.postMessageTarget, { hostStatus: 'ready' }); + postStatus(this.options.postMessageTarget, { type: 'hostStatus', hostStatus: 'ready' }); }, onError: () => { this.errorHandler(new Error('Sandbox initialization failed'), 'Sandbox could not be initialized'); @@ -201,7 +201,7 @@ export class Listener { } private renderInteractiveDocument(content: InteractiveDocument) { - postStatus(this.options.postMessageTarget, { hostStatus: 'compiling', details: 'Starting interactive document compilation' }); + postStatus(this.options.postMessageTarget, { type: 'hostStatus', hostStatus: 'compiling', details: 'Starting interactive document compilation' }); const markdown = targetMarkdown(content); this.renderMarkdown(markdown); } @@ -215,18 +215,18 @@ export class Listener { this.hideLoadingAndHelp(); try { - postStatus(this.options.postMessageTarget, { hostStatus: 'rendering', details: 'Starting markdown rendering' }); + postStatus(this.options.postMessageTarget, { type: 'hostStatus', hostStatus: 'rendering', details: 'Starting markdown rendering' }); if (!this.sandbox || !this.sandboxReady) { this.createSandbox(markdown); } else { this.sandbox.send(markdown); } - postStatus(this.options.postMessageTarget, { hostStatus: 'rendered', details: 'Markdown rendering completed successfully' }); + postStatus(this.options.postMessageTarget, { type: 'hostStatus', hostStatus: 'rendered', details: 'Markdown rendering completed successfully' }); } catch (error) { this.errorHandler( error, 'Error rendering markdown content' ); - postStatus(this.options.postMessageTarget, { hostStatus: 'error', details: `Rendering failed: ${error.message}` }); + postStatus(this.options.postMessageTarget, { type: 'hostStatus', hostStatus: 'error', details: `Rendering failed: ${error.message}` }); } } diff --git a/packages/vscode-resources/src/edit.ts b/packages/vscode-resources/src/edit.ts index 8b77b284..fac6e9d6 100644 --- a/packages/vscode-resources/src/edit.ts +++ b/packages/vscode-resources/src/edit.ts @@ -6,8 +6,10 @@ window.addEventListener('DOMContentLoaded', () => { let offlineDeps = ''; - if (event.data.type === 'setOfflineDeps') { - offlineDeps = event.data.offlineDeps; + const message = event.data as IDocs.common.EditorSetOfflineDependenciesMessage; + + if (message.type === 'editorSetOfflineDependencies') { + offlineDeps = message.offlineDeps; class OfflineSandbox extends IDocs.sandbox.Sandbox { constructor(element, markdown, options) { super(element, markdown, options); @@ -22,5 +24,10 @@ window.addEventListener('DOMContentLoaded', () => { } }); - vscode.postMessage({ type: 'getOfflineDeps' }, '*'); + const editorGetOfflineDependenciesMessage: IDocs.common.EditorGetOfflineDependenciesMessage = { + type: 'editorGetOfflineDependencies', + sender: 'webview', + }; + + vscode.postMessage(editorGetOfflineDependenciesMessage, '*'); }); diff --git a/packages/vscode-resources/src/idocs.d.ts b/packages/vscode-resources/src/idocs.d.ts index 485be7e4..e3911fab 100644 --- a/packages/vscode-resources/src/idocs.d.ts +++ b/packages/vscode-resources/src/idocs.d.ts @@ -1,4 +1,5 @@ import * as editor from 'editor'; +import * as common from 'common'; import * as compiler from '@microsoft/interactive-document-compiler'; import * as sandbox from '@microsoft/chartifact-sandbox'; import * as host from '@microsoft/interactive-document-host'; @@ -7,4 +8,4 @@ import * as schema from 'schema'; export as namespace IDocs; -export { editor, compiler, sandbox, host, markdown, schema }; +export { editor, compiler, sandbox, host, markdown, schema, common }; diff --git a/packages/vscode/src/web/command-edit.ts b/packages/vscode/src/web/command-edit.ts index 327ce094..e664122f 100644 --- a/packages/vscode/src/web/command-edit.ts +++ b/packages/vscode/src/web/command-edit.ts @@ -2,7 +2,7 @@ import * as vscode from 'vscode'; import { newPanel, WebViewWithUri } from './panel'; import { script, style } from './html'; import { getResourceContent } from './resources'; -import type { ReadyMessage, PageMessage, EditorMessage } from 'editor' with { 'resolution-mode': 'import' }; +import type { EditorPageMessage, EditorReadyMessage, EditorGetOfflineDependenciesMessage, EditorSetOfflineDependenciesMessage } from 'common' with { 'resolution-mode': 'import' }; /** * Manages the edit functionality for Interactive Documents @@ -54,13 +54,14 @@ export class EditManager { /** * Handles messages from the webview */ - private handleWebviewMessage(message: EditorMessage | { type: 'getOfflineDeps' }, fileUri: vscode.Uri, uriFsPath: string) { + private handleWebviewMessage(message: EditorPageMessage | EditorReadyMessage | EditorGetOfflineDependenciesMessage, fileUri: vscode.Uri, uriFsPath: string) { switch (message.type) { - case 'getOfflineDeps': { + case 'editorGetOfflineDependencies': { // Send offline dependencies to the webview if (this.current) { - this.current.panel.webview.postMessage({ - type: 'setOfflineDeps', + const setOfflineDependenciesMessage: EditorSetOfflineDependenciesMessage = { + type: 'editorSetOfflineDependencies', + sender: 'vscode', offlineDeps: style(getResourceContent('tabulator.min.css')) + script(getResourceContent('markdown-it.min.js')) + @@ -68,15 +69,16 @@ export class EditManager { script(getResourceContent('vega.min.js')) + script(getResourceContent('vega-lite.min.js')) + script(getResourceContent('tabulator.min.js')) - }); + } + this.current.panel.webview.postMessage(setOfflineDependenciesMessage); } break; } - case 'ready': { + case 'editorReady': { this.getFileContentAndRender(fileUri, uriFsPath); break; } - case 'page': { + case 'editorPage': { // Handle page updates from the editor this.handlePageUpdate(message, fileUri, uriFsPath); break; @@ -87,7 +89,7 @@ export class EditManager { /** * Handles page update messages from the webview editor */ - private async handlePageUpdate(message: PageMessage, fileUri: vscode.Uri, uriFsPath: string) { + private async handlePageUpdate(message: EditorPageMessage, fileUri: vscode.Uri, uriFsPath: string) { try { // Convert the page data to JSON string const jsonContent = JSON.stringify(message.page, null, 2); @@ -120,7 +122,7 @@ export class EditManager { const interactiveDocument = JSON.parse(jsonContent); this.render({ page: interactiveDocument, - type: 'page', + type: 'editorPage', sender: 'app' }); } catch (error) { @@ -131,7 +133,7 @@ export class EditManager { }); } - public render(renderRequestMessage: PageMessage) { + public render(renderRequestMessage: EditorPageMessage) { if (this.current && this.current.panel.visible) { this.current.panel.webview.postMessage(renderRequestMessage); } From 1e1aff79c466a70ed512f25d9051c4c1433e377a Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Fri, 25 Jul 2025 21:56:32 -0700 Subject: [PATCH 14/61] add approval handler --- packages/vscode-resources/src/html-json.ts | 32 ++++++++++++++++++- .../vscode-resources/src/html-markdown.ts | 30 +++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/vscode-resources/src/html-json.ts b/packages/vscode-resources/src/html-json.ts index c056fcaf..0a163cb8 100644 --- a/packages/vscode-resources/src/html-json.ts +++ b/packages/vscode-resources/src/html-json.ts @@ -3,7 +3,7 @@ window.addEventListener('DOMContentLoaded', () => { let sandbox: IDocs.sandbox.Sandbox; const render = () => { const json = textarea.value; - let markdown; + let markdown: string; try { const interactiveDocument = JSON.parse(json); if (typeof interactiveDocument !== 'object') { @@ -16,6 +16,36 @@ window.addEventListener('DOMContentLoaded', () => { } if (!sandbox) { sandbox = new IDocs.sandbox.Sandbox('main', markdown); + + 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); + } + }); + } else { sandbox.send(markdown); } diff --git a/packages/vscode-resources/src/html-markdown.ts b/packages/vscode-resources/src/html-markdown.ts index c72a77c7..a4b6fddd 100644 --- a/packages/vscode-resources/src/html-markdown.ts +++ b/packages/vscode-resources/src/html-markdown.ts @@ -1,6 +1,36 @@ window.addEventListener('DOMContentLoaded', () => { const textarea = document.getElementById('markdown-input') as HTMLTextAreaElement; const sandbox = new IDocs.sandbox.Sandbox('main', textarea.value); + + 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); + } + }); + textarea.addEventListener('input', () => { sandbox.send(textarea.value); }); From 056dd0087f19e1f87bd091a5ea32fc1f65cb9014 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sat, 26 Jul 2025 10:37:28 -0700 Subject: [PATCH 15/61] Add hydratesBefore property to Plugin interface and sort plugins by hydration order --- packages/markdown/src/factory.ts | 1 + packages/markdown/src/renderer.ts | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/markdown/src/factory.ts b/packages/markdown/src/factory.ts index 20b48e23..5f41e580 100644 --- a/packages/markdown/src/factory.ts +++ b/packages/markdown/src/factory.ts @@ -39,6 +39,7 @@ export interface IInstance { export interface Plugin { name: string; + hydratesBefore?: string; initializePlugin: (md: MarkdownIt) => void; fence?: (token: Token, idx: number) => string; hydrateComponent?: (renderer: Renderer, errorHandler: ErrorHandler) => Promise; diff --git a/packages/markdown/src/renderer.ts b/packages/markdown/src/renderer.ts index e418081e..af1c5f3d 100644 --- a/packages/markdown/src/renderer.ts +++ b/packages/markdown/src/renderer.ts @@ -94,8 +94,19 @@ export class Renderer { //loop through all the plugins and render them this.signalBus.log('Renderer', 'rendering DOM'); const hydrationPromises: Promise[] = []; - for (let i = 0; i < plugins.length; i++) { - const plugin = plugins[i]; + + //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]; 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 => { From 90431546f68f77be845f7019a245a347c2b1aa01 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sat, 26 Jul 2025 11:12:04 -0700 Subject: [PATCH 16/61] use json object, vaga-lite uses vega --- packages/markdown/src/plugins/checkbox.ts | 41 +++++----- packages/markdown/src/plugins/css.ts | 79 +++++++++---------- packages/markdown/src/plugins/dropdown.ts | 55 ++++++------- packages/markdown/src/plugins/image.ts | 71 ++++++++--------- packages/markdown/src/plugins/placeholders.ts | 6 +- packages/markdown/src/plugins/presets.ts | 31 +++----- packages/markdown/src/plugins/tabulator.ts | 64 ++++++++------- packages/markdown/src/plugins/util.ts | 13 ++- packages/markdown/src/plugins/vega-lite.ts | 44 +++++++++-- packages/markdown/src/plugins/vega.ts | 55 +++++-------- packages/markdown/src/resolver.ts | 57 ------------- 11 files changed, 234 insertions(+), 282 deletions(-) delete mode 100644 packages/markdown/src/resolver.ts diff --git a/packages/markdown/src/plugins/checkbox.ts b/packages/markdown/src/plugins/checkbox.ts index e1e7dddd..3588e263 100644 --- a/packages/markdown/src/plugins/checkbox.ts +++ b/packages/markdown/src/plugins/checkbox.ts @@ -5,7 +5,7 @@ import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; -import { getJsonScriptTag } from './util.js'; +import { getJsonScriptTag, pluginClassName } from './util.js'; interface CheckboxInstance { id: string; @@ -19,23 +19,25 @@ export interface CheckboxSpec { label?: string; } +const pluginName = 'checkbox'; +const className = pluginClassName(pluginName); + export const checkboxPlugin: Plugin = { - name: 'checkbox', - initializePlugin: (md) => definePlugin(md, 'checkbox'), - fence: (token, idx) => { - return sanitizedHTML('div', { class: 'checkbox' }, token.content.trim(), true); + name: pluginName, + initializePlugin: (md) => definePlugin(md, pluginName), + fence: token => { + return sanitizedHTML('div', { class: className }, token.content.trim(), true); }, hydrateComponent: async (renderer, errorHandler) => { const checkboxInstances: CheckboxInstance[] = []; - const containers = renderer.element.querySelectorAll('.checkbox'); + const containers = renderer.element.querySelectorAll(`.${className}`); for (const [index, container] of Array.from(containers).entries()) { - const scriptTag = getJsonScriptTag(container); - if (!scriptTag) continue; + const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); + if (!jsonObj) continue; - try { - const spec: CheckboxSpec = JSON.parse(scriptTag.textContent); + const spec: CheckboxSpec = jsonObj; - const html = ` + const html = `
`; - container.innerHTML = html; - const element = container.querySelector('input[type="checkbox"]') as HTMLInputElement; + container.innerHTML = html; + const element = container.querySelector('input[type="checkbox"]') as HTMLInputElement; - const checkboxInstance: CheckboxInstance = { id: `checkbox-${index}`, spec, element }; - checkboxInstances.push(checkboxInstance); - } catch (e) { - container.innerHTML = `
${e.toString()}
`; - errorHandler(e, 'Checkbox', index, 'parse', container); - continue; - } + const checkboxInstance: CheckboxInstance = { id: `${pluginName}-${index}`, spec, element }; + checkboxInstances.push(checkboxInstance); } - + const instances: IInstance[] = checkboxInstances.map((checkboxInstance) => { const { element, spec } = checkboxInstance; const initialSignals = [{ @@ -63,7 +60,7 @@ export const checkboxPlugin: Plugin = { priority: 1, isData: false, }]; - + return { ...checkboxInstance, initialSignals, diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index 37cbc457..229373f5 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -6,7 +6,7 @@ import { definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import * as Csstree from 'css-tree'; -import { getJsonScriptTag } from './util.js'; +import { getJsonScriptTag, pluginClassName } from './util.js'; // CSS Tree is expected to be available as a global variable declare const csstree: typeof Csstree; @@ -303,22 +303,25 @@ function categorizeCss(cssContent: string): CategorizedCss { return result; } +const pluginName = 'css'; +const className = pluginClassName(pluginName); + export const cssPlugin: Plugin = { - name: 'css', + name: pluginName, initializePlugin: (md) => { // Check for required css-tree dependency if (typeof csstree === 'undefined') { throw new Error('css-tree library is required for CSS plugin. Please include the css-tree script.'); } - definePlugin(md, 'css'); + definePlugin(md, pluginName); // Custom rule for CSS blocks md.block.ruler.before('fence', 'css_block', function (state, startLine, endLine) { const start = state.bMarks[startLine] + state.tShift[startLine]; const max = state.eMarks[startLine]; // Check if the block starts with "```css" - if (!state.src.slice(start, max).trim().startsWith('```css')) { + if (!state.src.slice(start, max).trim().startsWith(`\`\`\`${pluginName}`)) { return false; } @@ -332,7 +335,7 @@ export const cssPlugin: Plugin = { state.line = nextLine + 1; const token = state.push('fence', 'code', 0); - token.info = 'css'; + token.info = pluginName; token.content = state.getLines(startLine + 1, nextLine, state.blkIndent, true); token.map = [startLine, state.line]; @@ -345,13 +348,13 @@ export const cssPlugin: Plugin = { const token = tokens[idx]; const info = token.info.trim(); - if (info === 'css') { + if (info === pluginName) { const cssContent = token.content.trim(); // Parse and categorize CSS content const categorizedCss = categorizeCss(cssContent); - return sanitizedHTML('div', { class: 'css-component' }, JSON.stringify(categorizedCss), true); + return sanitizedHTML('div', { class: className }, JSON.stringify(categorizedCss), true); } // Fallback to original fence renderer @@ -364,50 +367,44 @@ export const cssPlugin: Plugin = { }, hydrateComponent: async (renderer, errorHandler) => { const cssInstances: { id: string; element: HTMLStyleElement }[] = []; - const containers = renderer.element.querySelectorAll('.css-component'); + const containers = renderer.element.querySelectorAll(`.${className}`); for (const [index, container] of Array.from(containers).entries()) { - const scriptTag = getJsonScriptTag(container); - if (!scriptTag) continue; + const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); + if (!jsonObj) continue; - try { - const categorizedCss: CategorizedCss = JSON.parse(scriptTag.textContent); - const comments: string[] = []; + const categorizedCss: CategorizedCss = jsonObj; + const comments: string[] = []; - // Log security issues found - if (categorizedCss.hasFlags) { - console.warn(`CSS security: Security issues detected in CSS`); - comments.push(``); - } + // Log security issues found + if (categorizedCss.hasFlags) { + console.warn(`CSS security: Security issues detected in CSS`); + comments.push(``); + } - // Generate and apply safe CSS - const safeCss = reconstituteCss(categorizedCss.atRules); + // Generate and apply safe CSS + const safeCss = reconstituteCss(categorizedCss.atRules); - if (safeCss.trim().length > 0) { - const styleElement = document.createElement('style'); - styleElement.type = 'text/css'; - styleElement.id = `idocs-css-${index}`; - styleElement.textContent = safeCss; + if (safeCss.trim().length > 0) { + const styleElement = document.createElement('style'); + styleElement.type = 'text/css'; + styleElement.id = `idocs-css-${index}`; + styleElement.textContent = safeCss; - // Apply to shadow DOM if available, otherwise document - const target = renderer.shadowRoot || document.head; - target.appendChild(styleElement); + // Apply to shadow DOM if available, otherwise document + const target = renderer.shadowRoot || document.head; + target.appendChild(styleElement); - comments.push(``); + comments.push(``); - cssInstances.push({ - id: `css-${index}`, - element: styleElement - }); - } else { - comments.push(``); - } - container.innerHTML = comments.join('\n'); - } catch (e) { - container.innerHTML = `
${e.toString()}
`; - errorHandler(e, 'CSS', index, 'parse', container); - continue; + cssInstances.push({ + id: `${pluginName}-${index}`, + element: styleElement + }); + } else { + comments.push(``); } + container.innerHTML = comments.join('\n'); } const instances: IInstance[] = cssInstances.map((cssInstance) => { diff --git a/packages/markdown/src/plugins/dropdown.ts b/packages/markdown/src/plugins/dropdown.ts index 51cf6e56..a0ea0546 100644 --- a/packages/markdown/src/plugins/dropdown.ts +++ b/packages/markdown/src/plugins/dropdown.ts @@ -5,7 +5,7 @@ import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; -import { getJsonScriptTag } from './util.js'; +import { getJsonScriptTag, pluginClassName } from './util.js'; interface DropdownInstance { id: string; @@ -29,23 +29,25 @@ export interface DropdownSpec { dynamicOptions?: DynamicDropdownOptions; } +const pluginName = 'dropdown'; +const className = pluginClassName(pluginName); + export const dropdownPlugin: Plugin = { - name: 'dropdown', - initializePlugin: (md) => definePlugin(md, 'dropdown'), - fence: (token, idx) => { - return sanitizedHTML('div', { class: 'dropdown' }, token.content.trim(), true); + name: pluginName, + initializePlugin: (md) => definePlugin(md, pluginName), + fence: token => { + return sanitizedHTML('div', { class: className }, token.content.trim(), true); }, hydrateComponent: async (renderer, errorHandler) => { const dropdownInstances: DropdownInstance[] = []; - const containers = renderer.element.querySelectorAll('.dropdown'); + const containers = renderer.element.querySelectorAll(`.${className}`); for (const [index, container] of Array.from(containers).entries()) { - const scriptTag = getJsonScriptTag(container); - if (!scriptTag) continue; + const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); + if (!jsonObj) continue; - try { - const spec: DropdownSpec = JSON.parse(scriptTag.textContent); + const spec: DropdownSpec = jsonObj; - const html = `
+ const html = `
`; - container.innerHTML = html; - const element = container.querySelector('select') as HTMLSelectElement; - - // Safely set the initial options - setSelectOptions(element, spec.multiple ?? false, spec.options ?? [], spec.value ?? (spec.multiple ? [] : "")); - - const dropdownInstance: DropdownInstance = { id: `dropdown-${index}`, spec, element }; - dropdownInstances.push(dropdownInstance); - } catch (e) { - container.innerHTML = `
${e.toString()}
`; - errorHandler(e, 'Dropdown', index, 'parse', container); - continue; - } + container.innerHTML = html; + const element = container.querySelector('select') as HTMLSelectElement; + + // Safely set the initial options + setSelectOptions(element, spec.multiple ?? false, spec.options ?? [], spec.value ?? (spec.multiple ? [] : "")); + + const dropdownInstance: DropdownInstance = { id: `${pluginName}-${index}`, spec, element }; + dropdownInstances.push(dropdownInstance); } const instances: IInstance[] = dropdownInstances.map((dropdownInstance, index) => { const { element, spec } = dropdownInstance; @@ -165,7 +162,7 @@ export const dropdownPlugin: Plugin = { function setSelectOptions(selectElement: HTMLSelectElement, multiple: boolean, options: string[], selected: string | string[]) { // Clear existing options selectElement.innerHTML = ''; - + if (!options || options.length === 0) { if (multiple) { if (Array.isArray(selected)) { @@ -181,16 +178,16 @@ function setSelectOptions(selectElement: HTMLSelectElement, multiple: boolean, o } } } - + if (!options || options.length === 0) { return; } - + options.forEach((optionValue) => { const optionElement = document.createElement('option'); optionElement.value = optionValue; optionElement.textContent = optionValue; // This safely escapes HTML - + let isSelected = false; if (multiple) { isSelected = (selected as string[] || []).includes(optionValue); @@ -198,7 +195,7 @@ function setSelectOptions(selectElement: HTMLSelectElement, multiple: boolean, o isSelected = selected === optionValue; } optionElement.selected = isSelected; - + selectElement.appendChild(optionElement); }); } diff --git a/packages/markdown/src/plugins/image.ts b/packages/markdown/src/plugins/image.ts index 2dd677e0..8f53daf9 100644 --- a/packages/markdown/src/plugins/image.ts +++ b/packages/markdown/src/plugins/image.ts @@ -5,7 +5,7 @@ import { definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; -import { getJsonScriptTag } from './util.js'; +import { getJsonScriptTag, pluginClassName } from './util.js'; export interface ImageSpec { srcSignalName: string; @@ -27,54 +27,53 @@ enum ImageOpacity { error = '0.5', } +const pluginName = 'image'; +const className = pluginClassName(pluginName); + export const imagePlugin: Plugin = { - name: 'image', - initializePlugin: (md) => definePlugin(md, 'image'), - fence: (token, idx) => { - return sanitizedHTML('div', { class: 'image' }, token.content.trim(), true); + name: pluginName, + initializePlugin: (md) => definePlugin(md, pluginName), + fence: token => { + return sanitizedHTML('div', { class: className }, token.content.trim(), true); }, hydrateComponent: async (renderer, errorHandler) => { const imageInstances: ImageInstance[] = []; - const containers = renderer.element.querySelectorAll('.image'); + const containers = renderer.element.querySelectorAll(`.${className}`); for (const [index, container] of Array.from(containers).entries()) { - const scriptTag = getJsonScriptTag(container); - if (!scriptTag) continue; - try { - const spec: ImageSpec = JSON.parse(scriptTag.textContent); - const element = document.createElement('img'); - const spinner = document.createElement('div'); - spinner.innerHTML = ` + const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); + if (!jsonObj) continue; + + const spec: ImageSpec = jsonObj; + const element = document.createElement('img'); + const spinner = document.createElement('div'); + spinner.innerHTML = ` `; - if (spec.alt) element.alt = spec.alt; - if (spec.width) element.width = spec.width; - if (spec.height) element.height = spec.height; - element.onload = () => { - spinner.style.display = 'none'; - element.style.opacity = ImageOpacity.full; - }; - element.onerror = () => { - spinner.style.display = 'none'; - element.style.opacity = ImageOpacity.error; - errorHandler(new Error('Image failed to load'), 'image', index, 'load', container, element.src); - }; + if (spec.alt) element.alt = spec.alt; + if (spec.width) element.width = spec.width; + if (spec.height) element.height = spec.height; + element.onload = () => { + spinner.style.display = 'none'; + element.style.opacity = ImageOpacity.full; + }; + element.onerror = () => { + spinner.style.display = 'none'; + element.style.opacity = ImageOpacity.error; + errorHandler(new Error('Image failed to load'), pluginName, index, 'load', container, element.src); + }; - (container as HTMLElement).style.position = 'relative'; - spinner.style.position = 'absolute'; - container.innerHTML = ''; - container.appendChild(spinner); - container.appendChild(element); + (container as HTMLElement).style.position = 'relative'; + spinner.style.position = 'absolute'; + container.innerHTML = ''; + container.appendChild(spinner); + container.appendChild(element); - const imageInstance: ImageInstance = { id: `image-${index}`, spec, element, spinner }; - imageInstances.push(imageInstance); - } catch (e) { - container.innerHTML = `
${e.toString()}
`; - errorHandler(e, 'Image', index, 'parse', container); - } + const imageInstance: ImageInstance = { id: `${pluginName}-${index}`, spec, element, spinner }; + imageInstances.push(imageInstance); } const instances: IInstance[] = imageInstances.map((imageInstance, index) => { const { element, spinner, id, spec } = imageInstance; diff --git a/packages/markdown/src/plugins/placeholders.ts b/packages/markdown/src/plugins/placeholders.ts index ce52cffd..fefa2fbd 100644 --- a/packages/markdown/src/plugins/placeholders.ts +++ b/packages/markdown/src/plugins/placeholders.ts @@ -34,8 +34,10 @@ function handleDynamicUrl(tokens: Token[], idx: number, attrName: string, elemen return token; } +const pluginName = 'placeholders'; + export const placeholdersPlugin: Plugin = { - name: 'placeholders', + name: pluginName, initializePlugin: async (md) => { // Custom plugin to handle dynamic placeholders md.use(function (md) { @@ -147,7 +149,7 @@ export const placeholdersPlugin: Plugin = { const instances: IInstance[] = [ { - id: 'placeholders', + id: pluginName, initialSignals, recieveBatch: async (batch) => { for (const key of Object.keys(batch)) { diff --git a/packages/markdown/src/plugins/presets.ts b/packages/markdown/src/plugins/presets.ts index 25fa1522..ac802ecd 100644 --- a/packages/markdown/src/plugins/presets.ts +++ b/packages/markdown/src/plugins/presets.ts @@ -5,7 +5,7 @@ import { Batch, definePlugin, IInstance, Plugin, PrioritizedSignal } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; -import { getJsonScriptTag } from './util.js'; +import { getJsonScriptTag, pluginClassName } from './util.js'; interface Preset { name: string; @@ -21,29 +21,24 @@ interface PresetsInstance { element: HTMLUListElement; } +const pluginName = 'presets'; +const className = pluginClassName(pluginName); + export const presetsPlugin: Plugin = { - name: 'presets', - initializePlugin: (md) => definePlugin(md, 'presets'), - fence: (token, idx) => { - const spec = JSON.parse(token.content.trim()); - return sanitizedHTML('div', { class: 'presets' }, JSON.stringify(spec), true); + name: pluginName, + initializePlugin: (md) => definePlugin(md, pluginName), + fence: token => { + return sanitizedHTML('div', { class: className }, token.content.trim(), true); }, hydrateComponent: async (renderer, errorHandler) => { const presetsInstances: PresetsInstance[] = []; - const containers = renderer.element.querySelectorAll('.presets'); + const containers = renderer.element.querySelectorAll(`.${className}`); for (const [index, container] of Array.from(containers).entries()) { - const scriptTag = getJsonScriptTag(container); - if (!scriptTag) continue; + const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); + if (!jsonObj) continue; - const id = `presets-${index}`; - let presets: Preset[]; - try { - presets = JSON.parse(scriptTag.textContent) as Preset[]; - } catch (e) { - container.innerHTML = `
${e.toString()}
`; - errorHandler(e, 'presets', index, 'parse', container); - continue; - } + const id = `${pluginName}-${index}`; + const presets = jsonObj as Preset[]; if (!Array.isArray(presets)) { container.innerHTML = '
Expected an array of presets
'; continue; diff --git a/packages/markdown/src/plugins/tabulator.ts b/packages/markdown/src/plugins/tabulator.ts index 9f633991..2cfdd2ea 100644 --- a/packages/markdown/src/plugins/tabulator.ts +++ b/packages/markdown/src/plugins/tabulator.ts @@ -7,7 +7,7 @@ import { defaultCommonOptions } from 'common'; import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { Tabulator as TabulatorType, Options as TabulatorOptions } from 'tabulator-tables'; -import { getJsonScriptTag } from './util.js'; +import { getJsonScriptTag, pluginClassName } from './util.js'; interface TabulatorInstance { id: string; @@ -23,49 +23,47 @@ export interface TabulatorSpec { declare const Tabulator: typeof TabulatorType; +const pluginName = 'tabulator'; +const className = pluginClassName(pluginName); + export const tabulatorPlugin: Plugin = { - name: 'tabulator', - initializePlugin: (md) => definePlugin(md, 'tabulator'), - fence: (token, idx) => { - return sanitizedHTML('div', { class: 'tabulator', style: 'box-sizing: border-box;' }, token.content.trim(), true); + name: pluginName, + initializePlugin: (md) => definePlugin(md, pluginName), + fence: token => { + return sanitizedHTML('div', { class: className, style: 'box-sizing: border-box;' }, token.content.trim(), true); }, hydrateComponent: async (renderer, errorHandler) => { const tabulatorInstances: TabulatorInstance[] = []; - const containers = renderer.element.querySelectorAll('.tabulator'); + const containers = renderer.element.querySelectorAll(`.${className}`); for (const [index, container] of Array.from(containers).entries()) { - const scriptTag = getJsonScriptTag(container); - if (!scriptTag) continue; - if (!Tabulator) { - errorHandler(new Error('Tabulator not found'), 'tabulator', index, 'init', container); + const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); + if (!jsonObj) continue; + + if (!Tabulator && index === 0) { + errorHandler(new Error('Tabulator not found'), pluginName, index, 'init', container); continue; } - try { - const spec: TabulatorSpec = JSON.parse(scriptTag.textContent); + const spec: TabulatorSpec = jsonObj; - let options: TabulatorOptions = { - autoColumns: true, - layout: 'fitColumns', - maxHeight: '200px', - }; - - //see if default options is an object with no properties - if (spec.options && Object.keys(spec.options).length > 0) { - options = spec.options; - } + let options: TabulatorOptions = { + autoColumns: true, + layout: 'fitColumns', + maxHeight: '200px', + }; - const table = new Tabulator(container as HTMLElement, options); - const tabulatorInstance: TabulatorInstance = { id: `tabulator-${index}`, spec, table, built: false }; - table.on('tableBuilt', () => { - table.off('tableBuilt'); - tabulatorInstance.built = true; - }); - tabulatorInstances.push(tabulatorInstance); - } catch (e) { - container.innerHTML = `
${e.toString()}
`; - errorHandler(e, 'tabulator', index, 'parse', container); - continue; + //see if default options is an object with no properties + if (spec.options && Object.keys(spec.options).length > 0) { + options = spec.options; } + + const table = new Tabulator(container as HTMLElement, options); + const tabulatorInstance: TabulatorInstance = { id: `${pluginName}-${index}`, spec, table, built: false }; + table.on('tableBuilt', () => { + table.off('tableBuilt'); + tabulatorInstance.built = true; + }); + tabulatorInstances.push(tabulatorInstance); } const dataNameSelectedSuffix = defaultCommonOptions.dataNameSelectedSuffix; const instances: IInstance[] = tabulatorInstances.map((tabulatorInstance, index) => { diff --git a/packages/markdown/src/plugins/util.ts b/packages/markdown/src/plugins/util.ts index 31e2c537..03524240 100644 --- a/packages/markdown/src/plugins/util.ts +++ b/packages/markdown/src/plugins/util.ts @@ -7,7 +7,7 @@ export function urlParam(urlParamName: string, value: any) { } } -export function getJsonScriptTag(container: Element): HTMLScriptElement | null { +export function getJsonScriptTag(container: Element, errorHandler: (error: Error) => void) { const scriptTag = container.previousElementSibling; if (scriptTag?.tagName !== 'SCRIPT' || scriptTag.getAttribute('type') !== 'application/json') { return null; @@ -15,5 +15,14 @@ export function getJsonScriptTag(container: Element): HTMLScriptElement | null { if (!scriptTag.textContent) { return null; } - return scriptTag as HTMLScriptElement; + try { + return JSON.parse(scriptTag.textContent); + } catch (error) { + errorHandler(error); + return null; + } +} + +export function pluginClassName(pluginName: string) { + return `chartifact-plugin-${pluginName}`; } diff --git a/packages/markdown/src/plugins/vega-lite.ts b/packages/markdown/src/plugins/vega-lite.ts index ffb8c522..bca16d4a 100644 --- a/packages/markdown/src/plugins/vega-lite.ts +++ b/packages/markdown/src/plugins/vega-lite.ts @@ -5,12 +5,46 @@ import { definePlugin, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; +import { getJsonScriptTag, pluginClassName } from './util.js'; +import { vegaPlugin } from './vega.js'; +import { compile } from 'vega-lite'; + +const pluginName = 'vega-lite'; +const className = pluginClassName(pluginName); export const vegaLitePlugin: Plugin = { - name: 'vega-lite', - initializePlugin: (md) => definePlugin(md, 'vega-lite'), - fence: (token, idx) => { - const vegaLiteId = `vega-lite-${idx}`; - return sanitizedHTML('div', { id: vegaLiteId, class: 'vega-chart' }, token.content.trim()); + name: pluginName, + 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; + + //compile to vega + try { + const { spec } = compile(jsonObj); + + //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); + + //append the html right after this container + container.insertAdjacentHTML('afterend', html); + + //the container can be removed + container.remove(); + + } catch (error) { + //did not compile + errorHandler(error, pluginName, index, 'compile', container); + } + } + + //return a promise that resolves to an empty array of IInstance + return Promise.resolve([]); }, }; diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index 771399bc..dae82004 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -7,10 +7,9 @@ import { changeset, parse, View, expressionFunction, LoggerInterface } from 'veg import { Batch, IInstance, Plugin, PrioritizedSignal, definePlugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { BaseSignal, InitSignal, NewSignal, Runtime, Spec, ValuesData } from 'vega-typings'; -import { Resolver, resolveSpec } from '../resolver.js'; import { ErrorHandler, Renderer } from '../renderer.js'; import { LogLevel } from '../signalbus.js'; -import { urlParam } from './util.js'; +import { getJsonScriptTag, pluginClassName, urlParam } from './util.js'; import { defaultCommonOptions } from 'common'; const ignoredSignals = ['width', 'height', 'padding', 'autosize', 'background', 'style', 'parent', 'datum', 'item', 'event', 'cursor']; @@ -30,12 +29,14 @@ interface VegaInstance extends SpecInit { needToRun?: boolean; } +const pluginName = 'vega'; +const className = pluginClassName(pluginName); + export const vegaPlugin: Plugin = { - name: 'vega', - initializePlugin: (md) => definePlugin(md, 'vega'), - fence: (token, idx) => { - const vegaId = `vega-${idx}`; - return sanitizedHTML('div', { id: vegaId, class: 'vega-chart' }, token.content.trim()); + name: pluginName, + initializePlugin: (md) => definePlugin(md, pluginName), + fence: token => { + return sanitizedHTML('div', { class: className }, token.content.trim(), true); }, hydrateComponent: async (renderer, errorHandler) => { //initialize the expressionFunction only once @@ -45,10 +46,13 @@ export const vegaPlugin: Plugin = { } const vegaInstances: VegaInstance[] = []; - const containers = renderer.element.querySelectorAll('.vega-chart'); + const containers = renderer.element.querySelectorAll(`.${className}`); const specInits: SpecInit[] = []; for (const [index, container] of Array.from(containers).entries()) { - const specInit = await createSpecInit(container, index, renderer, errorHandler); + const jsonObj = getJsonScriptTag(container, e => errorHandler(e, pluginName, index, 'parse', container)); + if (!jsonObj) continue; + + const specInit = createSpecInit(container, index, jsonObj); if (specInit) { specInits.push(specInit); } @@ -244,30 +248,7 @@ function recieveBatch(batch: Batch, renderer: Renderer, vegaInstance: VegaInstan return hasAnyChange; } -async function createSpecInit(container: Element, index: number, renderer: Renderer, errorHandler: ErrorHandler) { - if (!container.textContent) { - container.innerHTML = '
Expected a spec object or a url
'; - return; - } - - let result: Resolver; - try { - result = await resolveSpec(container.textContent); - } catch (e) { - container.innerHTML = `
${e.toString()}
`; - errorHandler(e, 'vega', index, 'resolve', container); - return; - } - if (result.error) { - container.innerHTML = `
${result.error.toString()}
`; - errorHandler(result.error, 'vega', index, 'resolve', container); - return; - } - if (!result.spec) { - container.innerHTML = '
Expected a spec object
'; - return; - } - const { spec } = result; +function createSpecInit(container: Element, index: number, spec: Spec) { const initialSignals: PrioritizedSignal[] = spec.signals?.map((signal: InitSignal | NewSignal) => { if (ignoredSignals.includes(signal.name)) return; let isData = isSignalDataBridge(signal as NewSignal); @@ -288,7 +269,7 @@ async function createSpecInit(container: Element, index: number, renderer: Rende async function createVegaInstance(specInit: SpecInit, renderer: Renderer, errorHandler: ErrorHandler) { const { container, index, initialSignals, spec } = specInit; - const id = `vega-${index}`; + const id = `${pluginName}-${index}`; let runtime: Runtime; let view: View; @@ -297,7 +278,7 @@ async function createVegaInstance(specInit: SpecInit, renderer: Renderer, errorH runtime = parse(spec); } catch (e) { container.innerHTML = `
${e.toString()}
`; - errorHandler(e, 'vega', index, 'parse', container); + errorHandler(e, pluginName, index, 'parse', container); return; } @@ -306,7 +287,7 @@ async function createVegaInstance(specInit: SpecInit, renderer: Renderer, errorH container, renderer: renderer.options.vegaRenderer, logger: new VegaLogger(error => { - errorHandler(error, 'vega', index, 'view', container); + errorHandler(error, pluginName, index, 'view', container); }), }); view.run(); @@ -323,7 +304,7 @@ async function createVegaInstance(specInit: SpecInit, renderer: Renderer, errorH } catch (e) { container.innerHTML = `
${e.toString()}
`; - errorHandler(e, 'vega', index, 'view', container); + errorHandler(e, pluginName, index, 'view', container); return; } diff --git a/packages/markdown/src/resolver.ts b/packages/markdown/src/resolver.ts deleted file mode 100644 index d98500a6..00000000 --- a/packages/markdown/src/resolver.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Spec } from 'vega'; -import { compile } from 'vega-lite'; - -export interface Resolver { - spec?: Spec; - error?: Error; -} - -export async function resolveSpec(textContent: string): Promise { - try { - const either = JSON.parse(textContent); - if (typeof either === 'object') { - return resolveToVega(either); - } else { - return { error: new Error(`Spec must be either a JSON object or a string url, found type ${typeof either}`) }; - } - } catch (error) { - //see if it is a url, then await to load the json for a spec - if (textContent.startsWith('http://') || textContent.startsWith('https://') || textContent.startsWith('//')) { - try { - const response = await fetch(textContent); - const either = await response.json(); - if (typeof either === 'object') { - return resolveToVega(either); - } else { - return { error: new Error(`Expected a JSON object, found type ${typeof either}`) }; - } - } catch (error) { - return { error }; - } - } else { - return { error: new Error('Spec string must be a url') }; - } - } -} - -function resolveToVega(either: any): Resolver { - if ('$schema' in either && typeof either.$schema === 'string') { - if (either.$schema.includes('vega-lite')) { - //compile to vega - try { - const runtime = compile(either); - const { spec } = runtime; - return { spec }; - } catch (error) { - //did not compile - return { error }; - } - } else if (either.$schema.includes('vega')) { - return { spec: either as Spec }; - } else { - return { error: new Error('$schema property must be a string with vega or vega-lite version.') }; - } - } else { - return { error: new Error('Missing $schema property, must be a string with vega or vega-lite version.') }; - } -} From bf91027e928b51a66cd459658495f7789d395660 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sat, 26 Jul 2025 12:29:23 -0700 Subject: [PATCH 17/61] suppress emptyoutdir message --- packages/compiler/vite.bundle.config.js | 1 + packages/editor/vite.bundle.config.js | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/compiler/vite.bundle.config.js b/packages/compiler/vite.bundle.config.js index 94c8e3b3..e3b4ddbd 100644 --- a/packages/compiler/vite.bundle.config.js +++ b/packages/compiler/vite.bundle.config.js @@ -19,6 +19,7 @@ export default defineConfig({ entry: './umd.ts', }, minify: false, + emptyOutDir: false, rollupOptions: { // External dependencies that the library expects consumers to provide external: ['vega', 'vega-lite'], diff --git a/packages/editor/vite.bundle.config.js b/packages/editor/vite.bundle.config.js index c35fcae0..9cb0e8aa 100644 --- a/packages/editor/vite.bundle.config.js +++ b/packages/editor/vite.bundle.config.js @@ -26,6 +26,7 @@ export default defineConfig({ entry: './umd.ts', }, minify: false, + emptyOutDir: false, rollupOptions: { // External dependencies that the library expects consumers to provide external: ['react', 'react-dom', 'vega', 'vega-lite'], From 5ffd210e7f4ddb56cc5da4c6b06794d6588a855f Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sat, 26 Jul 2025 18:05:15 -0700 Subject: [PATCH 18/61] use specs from schema --- docs/schema/idoc_v1.d.ts | 27 +++++++--- docs/schema/idoc_v1.json | 14 ++++-- packages/common/src/common.ts | 3 -- packages/compiler/src/loader.ts | 32 +++--------- packages/compiler/src/md.ts | 33 +++++-------- packages/compiler/src/spec.ts | 57 +++++++++++++--------- packages/markdown/src/plugins/checkbox.ts | 17 +++---- packages/markdown/src/plugins/dropdown.ts | 35 +++++-------- packages/markdown/src/plugins/image.ts | 6 +-- packages/markdown/src/plugins/presets.ts | 7 +-- packages/markdown/src/plugins/tabulator.ts | 23 ++++++--- packages/markdown/src/plugins/vega.ts | 6 +-- packages/schema/src/interactive.ts | 26 +++++++--- 13 files changed, 137 insertions(+), 149 deletions(-) diff --git a/docs/schema/idoc_v1.d.ts b/docs/schema/idoc_v1.d.ts index aa28e755..3b3f58c8 100644 --- a/docs/schema/idoc_v1.d.ts +++ b/docs/schema/idoc_v1.d.ts @@ -115,8 +115,10 @@ interface TextboxElement extends VariableControl { * Slider * prefer sliders over textbox for numbers. Never use for boolean values. */ -interface SliderElement extends VariableControl { +interface SliderElement extends VariableControl, SliderElementProps { type: 'slider'; +} +interface SliderElementProps { min: number; max: number; step: number; @@ -131,8 +133,10 @@ interface DynamicDropdownOptions { /** name of the field to use for options */ fieldName: string; } -interface DropdownElement extends VariableControl { +interface DropdownElement extends DropdownElementProps { type: 'dropdown'; +} +interface DropdownElementProps extends VariableControl { /** one of either options or dynamicOptions must be set */ options?: string[]; dynamicOptions?: DynamicDropdownOptions; @@ -167,19 +171,23 @@ interface ChartElement extends ElementBase { * Image element * use for displaying images or server-generated visualizations */ -interface ImageElement extends ElementBase { +interface ImageElement extends ElementBase, ImageElementProps { type: 'image'; + urlRef: UrlRef; +} +interface ImageElementProps { alt?: string; height?: number; width?: number; - urlRef: UrlRef; } /** * Presets * use for storing and applying preset batches of signal states */ -interface PresetsElement extends ElementBase { +interface PresetsElement extends ElementBase, PresetsElementProps { type: 'presets'; +} +interface PresetsElementProps { presets: Preset[]; } interface Preset { @@ -193,9 +201,12 @@ interface Preset { * Table * use for tabular data */ -interface TableElement extends ElementBase { +interface TableElement extends ElementBase, TableElementProps { type: 'table'; - dataSourceName: string; +} +interface TableElementProps { + input_dataSourceName: string; + output_dataSourceName: string; /** Tabulator options (must be serializable, so no callbacks allowed) */ options?: object; } @@ -236,4 +247,4 @@ type PageElement = MarkdownElement | InteractiveElement; type InteractiveDocumentWithSchema = InteractiveDocument & { $schema?: string; }; -export type { Calculation, ChartElement, ChartFull, ChartPlaceholder, ChartValue, CheckboxElement, DataLoader, DataLoaderBySpec, DataSource, DataSourceBase, DataSourceBaseFormat, DataSourceByDynamicURL, DataSourceByFile, DataSourceByJSON, DropdownElement, DynamicDropdownOptions, ElementBase, ElementGroup, ImageElement, InteractiveDocument, InteractiveDocumentWithSchema, InteractiveElement, Layout, MappedNameValuePairs, MarkdownElement, NameValuePairs, PageElement, Preset, PresetsElement, ReturnType, SliderElement, TableElement, TextboxElement, UrlRef, Variable, VariableControl, VariableID, VariableType, VariableValue, VariableValueArray, VariableValuePrimitive }; \ No newline at end of file +export type { Calculation, ChartElement, ChartFull, ChartPlaceholder, ChartValue, CheckboxElement, DataLoader, DataLoaderBySpec, DataSource, DataSourceBase, DataSourceBaseFormat, DataSourceByDynamicURL, DataSourceByFile, DataSourceByJSON, DropdownElement, DropdownElementProps, DynamicDropdownOptions, ElementBase, ElementGroup, ImageElement, ImageElementProps, InteractiveDocument, InteractiveDocumentWithSchema, InteractiveElement, Layout, MappedNameValuePairs, MarkdownElement, NameValuePairs, PageElement, Preset, PresetsElement, PresetsElementProps, ReturnType, SliderElement, SliderElementProps, TableElement, TableElementProps, TextboxElement, UrlRef, Variable, VariableControl, VariableID, VariableType, VariableValue, VariableValueArray, VariableValuePrimitive }; \ No newline at end of file diff --git a/docs/schema/idoc_v1.json b/docs/schema/idoc_v1.json index 2d6eb103..59b8331b 100644 --- a/docs/schema/idoc_v1.json +++ b/docs/schema/idoc_v1.json @@ -2520,8 +2520,8 @@ } }, "required": [ - "type", - "presets" + "presets", + "type" ], "type": "object" }, @@ -2820,21 +2820,25 @@ "additionalProperties": false, "description": "Table use for tabular data", "properties": { - "dataSourceName": { + "input_dataSourceName": { "type": "string" }, "options": { "description": "Tabulator options (must be serializable, so no callbacks allowed)", "type": "object" }, + "output_dataSourceName": { + "type": "string" + }, "type": { "const": "table", "type": "string" } }, "required": [ - "type", - "dataSourceName" + "input_dataSourceName", + "output_dataSourceName", + "type" ], "type": "object" }, diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts index 78db993f..0423f4e8 100644 --- a/packages/common/src/common.ts +++ b/packages/common/src/common.ts @@ -1,6 +1,4 @@ export interface RendererCommonOptions { - dataNameSelectedSuffix?: string; - /* dataSignalPrefix will set `isData` to true in the Signal Bus */ dataSignalPrefix?: string; @@ -8,7 +6,6 @@ export interface RendererCommonOptions { } export const defaultCommonOptions: RendererCommonOptions = { - dataNameSelectedSuffix: '_selected', dataSignalPrefix: 'data_signal:', groupClassName: 'group', }; diff --git a/packages/compiler/src/loader.ts b/packages/compiler/src/loader.ts index 1a4ad5f2..bf3ae584 100644 --- a/packages/compiler/src/loader.ts +++ b/packages/compiler/src/loader.ts @@ -1,25 +1,15 @@ import { DataSourceByDynamicURL, DataSourceByFile, DataSourceByJSON } from 'schema'; import { SignalRef, ValuesData } from 'vega'; import { VegaScope } from './scope.js'; +import { dataAsSignal, ensureDataAndSignalsArray } from './spec.js'; export function addStaticDataLoaderToSpec(vegaScope: VegaScope, dataSource: DataSourceByJSON | DataSourceByFile) { const { spec } = vegaScope; const { dataSourceName } = dataSource; - if (!spec.signals) { - spec.signals = []; - } - spec.signals.push( - { - name: dataSourceName, - update: `data('${dataSourceName}')` - } - ); + ensureDataAndSignalsArray(spec); - //real data goes to the beginning of the data array - if (!spec.data) { - spec.data = []; - } + spec.signals.push(dataAsSignal(dataSourceName)); const newData: ValuesData = { name: dataSourceName, @@ -36,6 +26,7 @@ export function addStaticDataLoaderToSpec(vegaScope: VegaScope, dataSource: Data newData.values = [dataSource.content]; } + //real data goes to the beginning of the data array spec.data.unshift(newData); } @@ -46,20 +37,11 @@ export function addDynamicDataLoaderToSpec(vegaScope: VegaScope, dataSource: Dat const urlSignal = vegaScope.createUrlSignal(dataSource.urlRef); const url: SignalRef = { signal: urlSignal.name }; - if (!spec.signals) { - spec.signals = []; - } - spec.signals.push( - { - name: dataSourceName, - update: `data('${dataSourceName}')` - } - ); + ensureDataAndSignalsArray(spec); + + spec.signals.push(dataAsSignal(dataSourceName)); //real data goes to the beginning of the data array - if (!spec.data) { - spec.data = []; - } spec.data.unshift({ name: dataSourceName, url, diff --git a/packages/compiler/src/md.ts b/packages/compiler/src/md.ts index 990eab63..18e10024 100644 --- a/packages/compiler/src/md.ts +++ b/packages/compiler/src/md.ts @@ -1,6 +1,6 @@ import { Spec as VegaSpec } from 'vega-typings'; import { TopLevelSpec as VegaLiteSpec } from "vega-lite"; -import { ChartFull, DataSource, ElementGroup, InteractiveDocument, Variable } from 'schema'; +import { ChartFull, DataSource, ElementGroup, InteractiveDocument, TableElement, Variable } from 'schema'; import { getChartType } from './util.js'; import { addDynamicDataLoaderToSpec, addStaticDataLoaderToSpec } from './loader.js'; import { Plugins } from '@microsoft/interactive-document-markdown'; @@ -38,7 +38,9 @@ export function targetMarkdown(page: InteractiveDocument) { mdSections.push(tickWrap('css', page.layout.css)); } - const vegaScope = dataLoaderMarkdown(dataLoaders.filter(dl => dl.type !== 'spec'), variables); + const tableElements = page.groups.flatMap(group => group.elements.filter(e => typeof e !== 'string' && e.type === 'table') as TableElement[]); + + const vegaScope = dataLoaderMarkdown(dataLoaders.filter(dl => dl.type !== 'spec'), variables, tableElements); for (const dataLoader of dataLoaders.filter(dl => dl.type === 'spec')) { mdSections.push(chartWrap(dataLoader.spec)); @@ -55,14 +57,13 @@ export function targetMarkdown(page: InteractiveDocument) { return markdown; } -function dataLoaderMarkdown(dataSources: DataSource[], variables: Variable[]) { +function dataLoaderMarkdown(dataSources: DataSource[], variables: Variable[], tableElements: TableElement[]) { //create a Vega spec with all variables - const spec = createSpecWithVariables(defaultCommonOptions.dataNameSelectedSuffix, variables); + const spec = createSpecWithVariables(variables, tableElements); const vegaScope = new VegaScope(spec); for (const dataSource of dataSources) { - switch (dataSource.type) { case 'json': { addStaticDataLoaderToSpec(vegaScope, dataSource); @@ -77,16 +78,6 @@ function dataLoaderMarkdown(dataSources: DataSource[], variables: Variable[]) { break; } } - - //TODO: only add this if there is a selectable table ? - - //add a special "-selected" data item - if (!spec.data) { - spec.data = []; - } - spec.data.unshift({ - name: dataSource.dataSourceName + defaultCommonOptions.dataNameSelectedSuffix, - }); } return vegaScope; @@ -112,7 +103,7 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve } case 'checkbox': { const cbSpec: Plugins.CheckboxSpec = { - name: element.variableId, + variableId: element.variableId, value: variables.find(v => v.variableId === element.variableId)?.initialValue as boolean, label: element.label, }; @@ -121,13 +112,13 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve } case 'dropdown': { const ddSpec: Plugins.DropdownSpec = { - name: element.variableId, + variableId: element.variableId, value: variables.find(v => v.variableId === element.variableId)?.initialValue as string | string[], label: element.label, }; if (element.dynamicOptions) { ddSpec.dynamicOptions = { - dataSignalName: element.dynamicOptions.dataSourceName, + dataSourceName: element.dynamicOptions.dataSourceName, fieldName: element.dynamicOptions.fieldName, }; } else { @@ -178,10 +169,8 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve break; } case 'table': { - const tableSpec: Plugins.TabulatorSpec = { - dataSignalName: element.dataSourceName, - options: element.options, - }; + const { input_dataSourceName, output_dataSourceName, options } = element; + const tableSpec: Plugins.TabulatorSpec = { input_dataSourceName, output_dataSourceName, options }; mdElements.push(jsonWrap('tabulator', JSON.stringify(tableSpec, null, 2))); break; } diff --git a/packages/compiler/src/spec.ts b/packages/compiler/src/spec.ts index 4a8b76d3..8146bc80 100644 --- a/packages/compiler/src/spec.ts +++ b/packages/compiler/src/spec.ts @@ -1,11 +1,11 @@ import { Spec as VegaSpec } from 'vega-typings'; -import { Variable, DataLoader } from "schema"; -import { SourceData, ValuesData, Signal } from "vega"; +import { Variable, DataLoader, TableElement } from "schema"; +import { SourceData, ValuesData, Signal, NewSignal } from "vega"; import { topologicalSort } from "./sort.js"; export const $schema = "https://vega.github.io/schema/vega/v5.json"; -export function createSpecWithVariables(dataNameSelectedSuffix: string, variables: Variable[], stubDataLoaders?: DataLoader[]) { +export function createSpecWithVariables(variables: Variable[], tableElements: TableElement[], stubDataLoaders?: DataLoader[]) { //preload with variables as signals const spec: VegaSpec = { @@ -14,24 +14,25 @@ export function createSpecWithVariables(dataNameSelectedSuffix: string, variable data: [], }; + //add table elements as data sources + tableElements.forEach((table) => { + const { output_dataSourceName } = table; + spec.signals.push(dataAsSignal(output_dataSourceName)); + spec.data.unshift({ + name: output_dataSourceName, + values: [], + }); + }); + //ensure (fake) data loaders are at the beginning of the data array if (stubDataLoaders) { //add data loaders as both signals and data sources stubDataLoaders.filter(dl => dl.type !== 'spec').forEach((dl) => { - spec.signals!.push({ - name: dl.dataSourceName, - value: `data('${dl.dataSourceName}')`, - }); - spec.data!.push({ + spec.signals.push(dataAsSignal(dl.dataSourceName)); + spec.data.push({ name: dl.dataSourceName, values: [], }); - - //add a special "-selected" data item - spec.data!.push({ - name: dl.dataSourceName + dataNameSelectedSuffix, - values: [], - }); }); } @@ -43,20 +44,14 @@ export function createSpecWithVariables(dataNameSelectedSuffix: string, variable source: v.calculation!.dependsOn || [], transform: dataFrameTransformations, }; - spec.data!.push(data); - if (!spec.signals) { - spec.signals = []; - } - spec.signals.push({ - name: v.variableId, - update: `data('${v.variableId}')`, - }); + spec.data.push(data); + spec.signals.push(dataAsSignal(v.variableId)); } else { const signal: Signal = { name: v.variableId, value: v.initialValue }; if (v.calculation) { signal.update = v.calculation!.vegaExpression; } - spec.signals!.push(signal); + spec.signals.push(signal); } }); @@ -71,3 +66,19 @@ function isDataframePipeline(variable: Variable): boolean { || (variable.calculation?.dataFrameTransformations !== undefined && variable.calculation.dataFrameTransformations.length > 0) ); } + +export function ensureDataAndSignalsArray(spec: VegaSpec) { + if (!spec.data) { + spec.data = []; + } + if (!spec.signals) { + spec.signals = []; + } +} + +export function dataAsSignal(name: string): NewSignal { + return { + name, + update: `data('${name}')` + }; +} diff --git a/packages/markdown/src/plugins/checkbox.ts b/packages/markdown/src/plugins/checkbox.ts index 3588e263..58fb850e 100644 --- a/packages/markdown/src/plugins/checkbox.ts +++ b/packages/markdown/src/plugins/checkbox.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +import { VariableControl } from 'schema'; import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { getJsonScriptTag, pluginClassName } from './util.js'; @@ -13,10 +14,8 @@ interface CheckboxInstance { element: HTMLInputElement; } -export interface CheckboxSpec { - name: string; +export interface CheckboxSpec extends VariableControl { value?: boolean; - label?: string; } const pluginName = 'checkbox'; @@ -40,8 +39,8 @@ export const checkboxPlugin: Plugin = { const html = `
`; @@ -55,7 +54,7 @@ export const checkboxPlugin: Plugin = { const instances: IInstance[] = checkboxInstances.map((checkboxInstance) => { const { element, spec } = checkboxInstance; const initialSignals = [{ - name: spec.name, + name: spec.variableId, value: spec.value || false, priority: 1, isData: false, @@ -65,8 +64,8 @@ export const checkboxPlugin: Plugin = { ...checkboxInstance, initialSignals, recieveBatch: async (batch) => { - if (batch[spec.name]) { - const value = batch[spec.name].value as boolean; + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value as boolean; element.checked = value; } }, @@ -75,7 +74,7 @@ export const checkboxPlugin: Plugin = { element.addEventListener('change', (e) => { const value = (e.target as HTMLInputElement).checked; const batch: Batch = { - [spec.name]: { + [spec.variableId]: { value, isData: false, }, diff --git a/packages/markdown/src/plugins/dropdown.ts b/packages/markdown/src/plugins/dropdown.ts index a0ea0546..00162e5f 100644 --- a/packages/markdown/src/plugins/dropdown.ts +++ b/packages/markdown/src/plugins/dropdown.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +import { DropdownElementProps } from 'schema'; import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { getJsonScriptTag, pluginClassName } from './util.js'; @@ -13,20 +14,8 @@ interface DropdownInstance { element: HTMLSelectElement; } -interface DynamicDropdownOptions { - dataSignalName: string; - fieldName: string; -} - -export interface DropdownSpec { - name: string; +export interface DropdownSpec extends DropdownElementProps { value?: string | string[]; - label?: string; - //one of either static or dynamic options must be set - options?: string[]; - multiple?: boolean; - size?: number; - dynamicOptions?: DynamicDropdownOptions; } const pluginName = 'dropdown'; @@ -50,8 +39,8 @@ export const dropdownPlugin: Plugin = { const html = `
@@ -68,14 +57,14 @@ export const dropdownPlugin: Plugin = { const instances: IInstance[] = dropdownInstances.map((dropdownInstance, index) => { const { element, spec } = dropdownInstance; const initialSignals = [{ - name: spec.name, + name: spec.variableId, value: spec.value || null, priority: 1, isData: false, }]; if (spec.dynamicOptions) { initialSignals.push({ - name: spec.dynamicOptions.dataSignalName, + name: spec.dynamicOptions.dataSourceName, value: null, priority: -1, isData: true, @@ -86,8 +75,8 @@ export const dropdownPlugin: Plugin = { initialSignals, recieveBatch: async (batch) => { const { dynamicOptions } = spec; - if (dynamicOptions?.dataSignalName) { - const newData = batch[dynamicOptions.dataSignalName]?.value as object[]; + if (dynamicOptions?.dataSourceName) { + const newData = batch[dynamicOptions.dataSourceName]?.value as object[]; if (newData) { //pluck the field from the data and add options to the select let hasFieldName = false; @@ -105,7 +94,7 @@ export const dropdownPlugin: Plugin = { const existingSelection = spec.multiple ? Array.from(element.selectedOptions).map(option => option.value) : element.value; setSelectOptions(element, spec.multiple ?? false, options, existingSelection); if (!spec.multiple) { - element.value = (batch[spec.name]?.value as string) || options[0]; + element.value = (batch[spec.variableId]?.value as string) || options[0]; } } else { //if the field doesn't exist, set the select to the first option @@ -118,8 +107,8 @@ export const dropdownPlugin: Plugin = { } } } - if (batch[spec.name]) { - const value = batch[spec.name].value as string | string[]; + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value as string | string[]; if (spec.multiple) { Array.from(element.options).forEach((option) => { option.selected = !!(value && Array.isArray(value) && value.includes(option.value)); @@ -136,7 +125,7 @@ export const dropdownPlugin: Plugin = { ? Array.from((e.target as HTMLSelectElement).selectedOptions).map(option => option.value) : (e.target as HTMLSelectElement).value; const batch: Batch = { - [spec.name]: { + [spec.variableId]: { value, isData: false, }, diff --git a/packages/markdown/src/plugins/image.ts b/packages/markdown/src/plugins/image.ts index 8f53daf9..33d8dce5 100644 --- a/packages/markdown/src/plugins/image.ts +++ b/packages/markdown/src/plugins/image.ts @@ -3,15 +3,13 @@ * Licensed under the MIT License. */ +import { ImageElementProps } from 'schema'; import { definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { getJsonScriptTag, pluginClassName } from './util.js'; -export interface ImageSpec { +export interface ImageSpec extends ImageElementProps { srcSignalName: string; - alt?: string; - width?: number; - height?: number; } interface ImageInstance { diff --git a/packages/markdown/src/plugins/presets.ts b/packages/markdown/src/plugins/presets.ts index ac802ecd..6f95f633 100644 --- a/packages/markdown/src/plugins/presets.ts +++ b/packages/markdown/src/plugins/presets.ts @@ -3,16 +3,11 @@ * Licensed under the MIT License. */ +import { Preset } from 'schema'; import { Batch, definePlugin, IInstance, Plugin, PrioritizedSignal } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { getJsonScriptTag, pluginClassName } from './util.js'; -interface Preset { - name: string; - description?: string; - state: { [signalName: string]: unknown }; -} - export type PresetsSpec = Preset[]; interface PresetsInstance { diff --git a/packages/markdown/src/plugins/tabulator.ts b/packages/markdown/src/plugins/tabulator.ts index 2cfdd2ea..d069c528 100644 --- a/packages/markdown/src/plugins/tabulator.ts +++ b/packages/markdown/src/plugins/tabulator.ts @@ -8,6 +8,7 @@ import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { Tabulator as TabulatorType, Options as TabulatorOptions } from 'tabulator-tables'; import { getJsonScriptTag, pluginClassName } from './util.js'; +import { TableElementProps } from 'schema'; interface TabulatorInstance { id: string; @@ -16,8 +17,7 @@ interface TabulatorInstance { built: boolean; } -export interface TabulatorSpec { - dataSignalName: string; +export interface TabulatorSpec extends TableElementProps { options?: TabulatorOptions; } @@ -46,6 +46,14 @@ export const tabulatorPlugin: Plugin = { const spec: TabulatorSpec = jsonObj; + if (!spec.input_dataSourceName || !spec.output_dataSourceName) { + errorHandler(new Error('Tabulator requires input_dataSourceName and output_dataSourceName'), pluginName, index, 'init', container); + continue; + } else if (spec.input_dataSourceName === spec.output_dataSourceName) { + errorHandler(new Error('Tabulator input_dataSourceName and output_dataSourceName cannot be the same'), pluginName, index, 'init', container); + continue; + } + let options: TabulatorOptions = { autoColumns: true, layout: 'fitColumns', @@ -65,17 +73,16 @@ export const tabulatorPlugin: Plugin = { }); tabulatorInstances.push(tabulatorInstance); } - const dataNameSelectedSuffix = defaultCommonOptions.dataNameSelectedSuffix; const instances: IInstance[] = tabulatorInstances.map((tabulatorInstance, index) => { const initialSignals = [{ - name: tabulatorInstance.spec.dataSignalName, + name: tabulatorInstance.spec.input_dataSourceName, value: null, priority: -1, isData: true, }]; if (tabulatorInstance.spec.options?.selectableRows) { initialSignals.push({ - name: `${tabulatorInstance.spec.dataSignalName}${dataNameSelectedSuffix}`, + name: tabulatorInstance.spec.output_dataSourceName, value: [], priority: -1, isData: true, @@ -85,7 +92,7 @@ export const tabulatorPlugin: Plugin = { ...tabulatorInstance, initialSignals, recieveBatch: async (batch) => { - const newData = batch[tabulatorInstance.spec.dataSignalName]?.value as object[]; + const newData = batch[tabulatorInstance.spec.input_dataSourceName]?.value as object[]; if (newData) { //make sure tabulator is ready before setting data if (!tabulatorInstance.built) { @@ -104,12 +111,12 @@ export const tabulatorPlugin: Plugin = { if (tabulatorInstance.spec.options?.selectableRows) { for (const { isData, signalName } of sharedSignals) { if (isData) { - const matchData = signalName === `${tabulatorInstance.spec.dataSignalName}${dataNameSelectedSuffix}`; + const matchData = signalName === tabulatorInstance.spec.output_dataSourceName; if (matchData) { tabulatorInstance.table.on('rowSelectionChanged', (e, rows) => { const selectedData = tabulatorInstance.table.getSelectedData(); const batch: Batch = { - [`${tabulatorInstance.spec.dataSignalName}${dataNameSelectedSuffix}`]: { + [tabulatorInstance.spec.output_dataSourceName]: { value: selectedData, isData: true, }, diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index dae82004..cb4f503b 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -73,11 +73,7 @@ export const vegaPlugin: Plugin = { if (!vegaInstance.spec.data) continue; for (const data of vegaInstance.spec.data) { //find a matching data signal - const dataSignal = dataSignals.find(signal => - (signal.name === data.name) //exact match - || - (`${signal.name}${defaultCommonOptions.dataNameSelectedSuffix}` === data.name), //match a selection from Tabulator - ); + const dataSignal = dataSignals.find(signal => (signal.name === data.name)); if (dataSignal) { //if we find a match, add it to our initialSignals vegaInstance.initialSignals.push({ diff --git a/packages/schema/src/interactive.ts b/packages/schema/src/interactive.ts index a5b5ef79..2480c68a 100644 --- a/packages/schema/src/interactive.ts +++ b/packages/schema/src/interactive.ts @@ -24,8 +24,10 @@ export interface TextboxElement extends VariableControl { * Slider * prefer sliders over textbox for numbers. Never use for boolean values. */ -export interface SliderElement extends VariableControl { +export interface SliderElement extends VariableControl, SliderElementProps { type: 'slider'; +} +export interface SliderElementProps { min: number; max: number; step: number; @@ -43,9 +45,10 @@ export interface DynamicDropdownOptions { fieldName: string; } -export interface DropdownElement extends VariableControl { +export interface DropdownElement extends DropdownElementProps { type: 'dropdown'; - +} +export interface DropdownElementProps extends VariableControl { /** one of either options or dynamicOptions must be set */ options?: string[]; dynamicOptions?: DynamicDropdownOptions; @@ -86,20 +89,24 @@ export interface ChartElement extends ElementBase { * Image element * use for displaying images or server-generated visualizations */ -export interface ImageElement extends ElementBase { +export interface ImageElement extends ElementBase, ImageElementProps { type: 'image'; + urlRef: UrlRef; +} +export interface ImageElementProps { alt?: string; height?: number; width?: number; - urlRef: UrlRef; } /** * Presets * use for storing and applying preset batches of signal states */ -export interface PresetsElement extends ElementBase { +export interface PresetsElement extends ElementBase, PresetsElementProps { type: 'presets'; +} +export interface PresetsElementProps { presets: Preset[]; } @@ -113,9 +120,12 @@ export interface Preset { * Table * use for tabular data */ -export interface TableElement extends ElementBase { +export interface TableElement extends ElementBase, TableElementProps { type: 'table'; - dataSourceName: string; +} +export interface TableElementProps { + input_dataSourceName: string; + output_dataSourceName: string; /** Tabulator options (must be serializable, so no callbacks allowed) */ options?: object; From 7955bf920df5ca85563d4c3e96cbb0d56785370b Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sat, 26 Jul 2025 18:06:56 -0700 Subject: [PATCH 19/61] fix missing semicolon --- packages/vscode/src/web/command-edit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode/src/web/command-edit.ts b/packages/vscode/src/web/command-edit.ts index e664122f..31964d80 100644 --- a/packages/vscode/src/web/command-edit.ts +++ b/packages/vscode/src/web/command-edit.ts @@ -69,7 +69,7 @@ export class EditManager { script(getResourceContent('vega.min.js')) + script(getResourceContent('vega-lite.min.js')) + script(getResourceContent('tabulator.min.js')) - } + }; this.current.panel.webview.postMessage(setOfflineDependenciesMessage); } break; From 98133ad38fc4801ab12a274b9b4b9c6e491941c1 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sat, 26 Jul 2025 19:39:01 -0700 Subject: [PATCH 20/61] fix for schema --- packages/editor/src/app.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/app.tsx b/packages/editor/src/app.tsx index 09b27e36..aecf5c11 100644 --- a/packages/editor/src/app.tsx +++ b/packages/editor/src/app.tsx @@ -175,7 +175,8 @@ const initialPage: InteractiveDocument = { "# Seattle Weather\n\nData table:", { "type": "table", - "dataSourceName": "seattle_weather", + "input_dataSourceName": "seattle_weather", + "output_dataSourceName": "seattle_weather_selected", "options": {} }, "Here is a stacked bar chart of Seattle weather:\nEach bar represents the count of weather types for each month.\nThe colors distinguish between different weather conditions such as sun, fog, drizzle, rain, and snow.", From e090d64bfdbcd68f185a896001a77f5a22e8b1ad Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Sun, 27 Jul 2025 12:10:57 -0700 Subject: [PATCH 21/61] 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 22/61] 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 23/61] 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 24/61] 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 25/61] 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 26/61] 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 27/61] 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 28/61] 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 29/61] 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 30/61] 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 31/61] 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 32/61] 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 33/61] 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 34/61] 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 35/61] 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 36/61] 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 37/61] 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 38/61] 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 39/61] 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 40/61] 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; }, From 2481390f4f9b2c09a470ffffc1928e284a851f0d Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 10:01:57 -0700 Subject: [PATCH 41/61] expose AppProps, previewer optional --- packages/editor/src/app.tsx | 2 +- packages/editor/src/editor.tsx | 4 ++-- packages/editor/src/index.ts | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/editor/src/app.tsx b/packages/editor/src/app.tsx index 72f5e3d1..7d5cc0b0 100644 --- a/packages/editor/src/app.tsx +++ b/packages/editor/src/app.tsx @@ -4,7 +4,7 @@ import { Previewer } from '@microsoft/chartifact-sandbox'; import { EditorPageMessage, EditorReadyMessage, SpecReview, SandboxedPreHydrateMessage } from "common"; export interface AppProps { - previewer: typeof Previewer; + previewer?: typeof Previewer; onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; } diff --git a/packages/editor/src/editor.tsx b/packages/editor/src/editor.tsx index 76c9caa9..24d05c6f 100644 --- a/packages/editor/src/editor.tsx +++ b/packages/editor/src/editor.tsx @@ -5,7 +5,7 @@ import { EditorPageMessage, EditorReadyMessage, SpecReview, SandboxedPreHydrateM export interface EditorProps { postMessageTarget?: Window; - previewer: typeof Previewer; + previewer?: typeof Previewer; onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; } @@ -77,7 +77,7 @@ export function Editor(props: EditorProps) { export interface EditorViewProps { page: InteractiveDocument; postMessageTarget: Window; - previewer: typeof Previewer; + previewer?: typeof Previewer; onApprove: (message: SandboxedPreHydrateMessage) => SpecReview<{}>[]; } diff --git a/packages/editor/src/index.ts b/packages/editor/src/index.ts index d2dfdc30..d5a90930 100644 --- a/packages/editor/src/index.ts +++ b/packages/editor/src/index.ts @@ -1,3 +1,2 @@ -export { Editor } from './editor.js'; -export { App } from './app.js'; -export type { EditorProps } from './editor.js'; +export { Editor, type EditorProps } from './editor.js'; +export { App, type AppProps } from './app.js'; From 76fb4dd0a26b9e3087b722671514c79e5925f290 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 10:13:08 -0700 Subject: [PATCH 42/61] add appProps to sandbox script for component rendering --- packages/editor/dev/index.ts | 8 ++++++++ packages/editor/index.html | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 packages/editor/dev/index.ts diff --git a/packages/editor/dev/index.ts b/packages/editor/dev/index.ts new file mode 100644 index 00000000..39b6089e --- /dev/null +++ b/packages/editor/dev/index.ts @@ -0,0 +1,8 @@ +import { AppProps } from '../src/app.js'; +export const appProps: AppProps = { + onApprove: (message) => { + //TODO policy to approve unapproved on localhost + const { specs } = message; + return specs; + } +}; diff --git a/packages/editor/index.html b/packages/editor/index.html index cb8abe12..f5c3834e 100644 --- a/packages/editor/index.html +++ b/packages/editor/index.html @@ -23,8 +23,9 @@ From df822d713531d67d739545addd8f356fa4ebd2e2 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 10:16:36 -0700 Subject: [PATCH 43/61] web package from typescript --- docs/assets/js/edit.js | 12 ++++++++++++ docs/{view => assets/js}/view.js | 0 docs/edit/index.html | 6 ++---- docs/view/index.html | 2 +- package-lock.json | 8 ++++++++ packages/vscode-resources/package.json | 1 - packages/web/package.json | 15 +++++++++++++++ packages/web/src/edit.ts | 15 +++++++++++++++ packages/web/src/idocs.d.ts | 11 +++++++++++ packages/web/src/react-dom.d.ts | 3 +++ packages/{vscode-resources => web}/src/view.ts | 0 packages/web/tsconfig.json | 15 +++++++++++++++ 12 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 docs/assets/js/edit.js rename docs/{view => assets/js}/view.js (100%) create mode 100644 packages/web/package.json create mode 100644 packages/web/src/edit.ts create mode 100644 packages/web/src/idocs.d.ts create mode 100644 packages/web/src/react-dom.d.ts rename packages/{vscode-resources => web}/src/view.ts (100%) create mode 100644 packages/web/tsconfig.json diff --git a/docs/assets/js/edit.js b/docs/assets/js/edit.js new file mode 100644 index 00000000..a299852b --- /dev/null +++ b/docs/assets/js/edit.js @@ -0,0 +1,12 @@ +window.addEventListener('DOMContentLoaded', () => { + const appProps = { + onApprove: (message) => { + // TODO look through each and override policy to approve unapproved + // policy from vscode settings + const { specs } = message; + return specs; + } + }; + const root = ReactDOM.createRoot(document.getElementById("app")); + root.render(React.createElement(IDocs.editor.App, appProps)); +}); diff --git a/docs/view/view.js b/docs/assets/js/view.js similarity index 100% rename from docs/view/view.js rename to docs/assets/js/view.js diff --git a/docs/edit/index.html b/docs/edit/index.html index b02a4bd8..50a083a3 100644 --- a/docs/edit/index.html +++ b/docs/edit/index.html @@ -14,11 +14,9 @@
+ + - \ No newline at end of file diff --git a/docs/view/index.html b/docs/view/index.html index a95cd9fc..3080e283 100644 --- a/docs/view/index.html +++ b/docs/view/index.html @@ -87,7 +87,7 @@

Interactive Document Host

- + diff --git a/package-lock.json b/package-lock.json index bec41319..4876f562 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12744,6 +12744,10 @@ "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", "dev": true }, + "node_modules/web": { + "resolved": "packages/web", + "link": true + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -13238,6 +13242,10 @@ "packages/vscode-resources": { "version": "1.0.0", "license": "MIT" + }, + "packages/web": { + "version": "1.0.0", + "license": "MIT" } } } diff --git a/packages/vscode-resources/package.json b/packages/vscode-resources/package.json index de79430b..0fa9da5c 100644 --- a/packages/vscode-resources/package.json +++ b/packages/vscode-resources/package.json @@ -7,7 +7,6 @@ "clean": "rimraf dist", "tsc": "tsc -p .", "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", diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 00000000..fb102fdb --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,15 @@ +{ + "name": "web", + "private": true, + "version": "1.0.0", + "main": "index.js", + "scripts": { + "clean": "rimraf dist", + "tsc": "tsc -p .", + "build": "npm run tsc", + "build:07": "npm run build" + }, + "author": "Dan Marshall", + "license": "MIT", + "description": "" +} diff --git a/packages/web/src/edit.ts b/packages/web/src/edit.ts new file mode 100644 index 00000000..51395eba --- /dev/null +++ b/packages/web/src/edit.ts @@ -0,0 +1,15 @@ +window.addEventListener('DOMContentLoaded', () => { + + const appProps: IDocs.editor.AppProps = { + onApprove: (message) => { + // TODO look through each and override policy to approve unapproved + // policy from vscode settings + const { specs } = message; + return specs; + } + }; + + const root = ReactDOM.createRoot(document.getElementById("app")); + root.render(React.createElement(IDocs.editor.App, appProps)); + +}); diff --git a/packages/web/src/idocs.d.ts b/packages/web/src/idocs.d.ts new file mode 100644 index 00000000..e3911fab --- /dev/null +++ b/packages/web/src/idocs.d.ts @@ -0,0 +1,11 @@ +import * as editor from 'editor'; +import * as common from 'common'; +import * as compiler from '@microsoft/interactive-document-compiler'; +import * as sandbox from '@microsoft/chartifact-sandbox'; +import * as host from '@microsoft/interactive-document-host'; +import * as markdown from '@microsoft/interactive-document-markdown'; +import * as schema from 'schema'; + +export as namespace IDocs; + +export { editor, compiler, sandbox, host, markdown, schema, common }; diff --git a/packages/web/src/react-dom.d.ts b/packages/web/src/react-dom.d.ts new file mode 100644 index 00000000..145ef443 --- /dev/null +++ b/packages/web/src/react-dom.d.ts @@ -0,0 +1,3 @@ +import { createRoot, Root } from 'react-dom/client'; +export { createRoot, Root }; +export as namespace ReactDOM; diff --git a/packages/vscode-resources/src/view.ts b/packages/web/src/view.ts similarity index 100% rename from packages/vscode-resources/src/view.ts rename to packages/web/src/view.ts diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json new file mode 100644 index 00000000..9bdf8442 --- /dev/null +++ b/packages/web/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "outDir": "../../docs/assets/js", + "module": "commonjs", + "skipLibCheck": true, + "target": "esnext", + "lib": [ + "dom", + "esnext" + ] + }, + "include": [ + "src" + ] +} From 6b57afc36e4c8c3a5dd93c6ff7354289a7fd032f Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 10:16:57 -0700 Subject: [PATCH 44/61] remove typeRoots configuration from tsconfig files --- packages/host-test/tsconfig.json | 3 --- packages/sandbox-resources/tsconfig.json | 3 --- packages/vscode-resources/tsconfig.json | 3 --- 3 files changed, 9 deletions(-) diff --git a/packages/host-test/tsconfig.json b/packages/host-test/tsconfig.json index af8a78e9..96012918 100644 --- a/packages/host-test/tsconfig.json +++ b/packages/host-test/tsconfig.json @@ -2,9 +2,6 @@ "compilerOptions": { "outDir": "dist", "module": "commonjs", - "typeRoots": [ - "./src" - ], "skipLibCheck": true, "target": "esnext", "lib": [ diff --git a/packages/sandbox-resources/tsconfig.json b/packages/sandbox-resources/tsconfig.json index af8a78e9..96012918 100644 --- a/packages/sandbox-resources/tsconfig.json +++ b/packages/sandbox-resources/tsconfig.json @@ -2,9 +2,6 @@ "compilerOptions": { "outDir": "dist", "module": "commonjs", - "typeRoots": [ - "./src" - ], "skipLibCheck": true, "target": "esnext", "lib": [ diff --git a/packages/vscode-resources/tsconfig.json b/packages/vscode-resources/tsconfig.json index af8a78e9..96012918 100644 --- a/packages/vscode-resources/tsconfig.json +++ b/packages/vscode-resources/tsconfig.json @@ -2,9 +2,6 @@ "compilerOptions": { "outDir": "dist", "module": "commonjs", - "typeRoots": [ - "./src" - ], "skipLibCheck": true, "target": "esnext", "lib": [ From 0d36adc39ddcec516ed48c3414f30ccac0614e24 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 10:20:33 -0700 Subject: [PATCH 45/61] change console.warn to console.debug --- packages/sandbox-resources/src/sandboxed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sandbox-resources/src/sandboxed.ts b/packages/sandbox-resources/src/sandboxed.ts index 21030183..9d45fe99 100644 --- a/packages/sandbox-resources/src/sandboxed.ts +++ b/packages/sandbox-resources/src/sandboxed.ts @@ -68,7 +68,7 @@ document.addEventListener('DOMContentLoaded', () => { renderer.hydrate(flags); } } else { - console.warn('Received sandbox approval for an outdated transaction:', message.transactionId, transactionIndex); + console.debug('Received sandbox approval for an outdated transaction:', message.transactionId, transactionIndex); } break; } From f4a26f4171600e3d31b446d585bb96d7e4d2e117 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 10:22:02 -0700 Subject: [PATCH 46/61] change comment --- docs/assets/js/edit.js | 3 +-- packages/web/src/edit.ts | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/assets/js/edit.js b/docs/assets/js/edit.js index a299852b..5fb9198f 100644 --- a/docs/assets/js/edit.js +++ b/docs/assets/js/edit.js @@ -1,8 +1,7 @@ window.addEventListener('DOMContentLoaded', () => { const appProps = { onApprove: (message) => { - // TODO look through each and override policy to approve unapproved - // policy from vscode settings + // 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/web/src/edit.ts b/packages/web/src/edit.ts index 51395eba..defb5832 100644 --- a/packages/web/src/edit.ts +++ b/packages/web/src/edit.ts @@ -2,8 +2,7 @@ window.addEventListener('DOMContentLoaded', () => { const appProps: IDocs.editor.AppProps = { onApprove: (message) => { - // TODO look through each and override policy to approve unapproved - // policy from vscode settings + // TODO look through each spec and override policy to approve unapproved for https://microsoft.github.io/chartifact/ const { specs } = message; return specs; } From 7b8a96b09b190ef0a2d35e12df7ed8821fdb4c88 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 10:46:01 -0700 Subject: [PATCH 47/61] common render function --- packages/markdown/test/index.html | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/markdown/test/index.html b/packages/markdown/test/index.html index bdf06ab7..2590b6f2 100644 --- a/packages/markdown/test/index.html +++ b/packages/markdown/test/index.html @@ -343,10 +343,15 @@ From b10e7bba184cfb74aaaaa3cbc48d338810a3e7dd Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 10:49:40 -0700 Subject: [PATCH 48/61] show specs in console --- packages/markdown/test/index.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/markdown/test/index.html b/packages/markdown/test/index.html index 2590b6f2..b4831522 100644 --- a/packages/markdown/test/index.html +++ b/packages/markdown/test/index.html @@ -345,7 +345,13 @@ const renderer = new IDocs.markdown.Renderer(document.getElementById('content')); function render() { - renderer.render(textarea.value); + //renderer.render(textarea.value); + + const html = renderer.renderHtml(textarea.value); + renderer.element.innerHTML = html; + const specs = renderer.hydrateSpecs(); + console.log('Specs:', specs); + renderer.hydrate(specs); } textarea.addEventListener('input', render); From 58a937d41c977bcb6248165ab8e8cf0254dafbd3 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 11:29:28 -0700 Subject: [PATCH 49/61] reset renderer before rendering HTML --- packages/markdown/test/index.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/markdown/test/index.html b/packages/markdown/test/index.html index b4831522..4e96aa5e 100644 --- a/packages/markdown/test/index.html +++ b/packages/markdown/test/index.html @@ -346,7 +346,8 @@ function render() { //renderer.render(textarea.value); - + + renderer.reset(); const html = renderer.renderHtml(textarea.value); renderer.element.innerHTML = html; const specs = renderer.hydrateSpecs(); From 9cb5f6abf541cd780921d2c2f06187a4daaabf81 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 11:36:49 -0700 Subject: [PATCH 50/61] use flaggable specs --- packages/markdown/src/plugins/checkbox.ts | 26 +++++++-------- packages/markdown/src/plugins/config.ts | 39 +++++++++++----------- packages/markdown/src/plugins/css.ts | 12 +++---- packages/markdown/src/plugins/dropdown.ts | 23 ++++++------- packages/markdown/src/plugins/image.ts | 23 ++++++------- packages/markdown/src/plugins/presets.ts | 23 ++++++------- packages/markdown/src/plugins/tabulator.ts | 35 ++++++++++--------- packages/markdown/src/plugins/vega.ts | 12 +++---- 8 files changed, 97 insertions(+), 96 deletions(-) diff --git a/packages/markdown/src/plugins/checkbox.ts b/packages/markdown/src/plugins/checkbox.ts index 58fb850e..4def01dc 100644 --- a/packages/markdown/src/plugins/checkbox.ts +++ b/packages/markdown/src/plugins/checkbox.ts @@ -4,9 +4,9 @@ */ import { VariableControl } from 'schema'; -import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; -import { sanitizedHTML } from '../sanitize.js'; +import { Batch, IInstance, Plugin } from '../factory.js'; import { getJsonScriptTag, pluginClassName } from './util.js'; +import { flaggableJsonPlugin } from './config.js'; interface CheckboxInstance { id: string; @@ -21,20 +21,18 @@ export interface CheckboxSpec extends VariableControl { const pluginName = 'checkbox'; const className = pluginClassName(pluginName); -export const checkboxPlugin: Plugin = { - name: pluginName, - initializePlugin: (md) => definePlugin(md, pluginName), - fence: token => { - return sanitizedHTML('div', { class: className }, token.content.trim(), true); - }, - hydrateComponent: async (renderer, errorHandler) => { +export const checkboxPlugin: Plugin = { + ...flaggableJsonPlugin(pluginName, className), + hydrateComponent: async (renderer, errorHandler, specs) => { const checkboxInstances: CheckboxInstance[] = []; - 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 (let index = 0; index < specs.length; index++) { + const specReview = specs[index]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(`#${specReview.containerId}`); - const spec: CheckboxSpec = jsonObj; + const spec: CheckboxSpec = specReview.approvedSpec; const html = `
diff --git a/packages/markdown/src/plugins/config.ts b/packages/markdown/src/plugins/config.ts index c92e25a3..48df3962 100644 --- a/packages/markdown/src/plugins/config.ts +++ b/packages/markdown/src/plugins/config.ts @@ -3,38 +3,39 @@ import { sanitizedHTML } from "../sanitize.js"; import { getJsonScriptTag } from "./util.js"; import { SpecReview } from 'common'; -export function flaggableJsonPlugin(pluginName: string, className: string, flagger?: (spec: T) => RawFlaggableSpec) { +export function flaggableJsonPlugin(pluginName: string, className: string, flagger?: (spec: T) => RawFlaggableSpec, attrs?: object) { const plugin: Plugin = { name: pluginName, initializePlugin: (md) => definePlugin(md, pluginName), fence: (token, index) => { let json = token.content.trim(); - if (flagger) { - let spec: T; - let flaggableSpec: RawFlaggableSpec; - try { - spec = JSON.parse(json); - } catch (e) { - flaggableSpec = { - spec: null, - hasFlags: true, - reasons: [`malformed JSON`], - }; - } - if (spec) { + let spec: T; + let flaggableSpec: RawFlaggableSpec; + try { + spec = JSON.parse(json); + } catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reasons: [`malformed JSON`], + }; + } + if (spec) { + if (flagger) { flaggableSpec = flagger(spec); + } else { + flaggableSpec = { spec }; } - if (flaggableSpec) { - json = JSON.stringify(flaggableSpec); - } } - return sanitizedHTML('div', { class: className, id: `${pluginName}-${index}` }, json, true); + if (flaggableSpec) { + json = JSON.stringify(flaggableSpec); + } + return sanitizedHTML('div', { class: className, id: `${pluginName}-${index}`, ...attrs }, json, true); }, hydrateSpecs: (renderer, errorHandler) => { 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: SpecReview = { approvedSpec: null, pluginName, containerId: container.id }; diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index 3c254408..487f6ca8 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -374,17 +374,17 @@ export const cssPlugin: Plugin = { } }; }, - hydrateComponent: async (renderer, errorHandler, configContainers) => { + hydrateComponent: async (renderer, errorHandler, specs) => { const cssInstances: { id: string; element: HTMLStyleElement }[] = []; - for (let index = 0; index < configContainers.length; index++) { - const configContainer = configContainers[index]; - if (!configContainer.approvedSpec) { + for (let index = 0; index < specs.length; index++) { + const specReview = specs[index]; + if (!specReview.approvedSpec) { continue; } - const container = renderer.element.querySelector(`#${configContainer.containerId}`); + const container = renderer.element.querySelector(`#${specReview.containerId}`); - const categorizedCss = configContainer.approvedSpec; + const categorizedCss = specReview.approvedSpec; const comments: string[] = []; // Generate and apply safe CSS diff --git a/packages/markdown/src/plugins/dropdown.ts b/packages/markdown/src/plugins/dropdown.ts index 00162e5f..5c0083c5 100644 --- a/packages/markdown/src/plugins/dropdown.ts +++ b/packages/markdown/src/plugins/dropdown.ts @@ -7,6 +7,7 @@ import { DropdownElementProps } from 'schema'; import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { getJsonScriptTag, pluginClassName } from './util.js'; +import { flaggableJsonPlugin } from './config.js'; interface DropdownInstance { id: string; @@ -21,20 +22,18 @@ export interface DropdownSpec extends DropdownElementProps { const pluginName = 'dropdown'; const className = pluginClassName(pluginName); -export const dropdownPlugin: Plugin = { - name: pluginName, - initializePlugin: (md) => definePlugin(md, pluginName), - fence: token => { - return sanitizedHTML('div', { class: className }, token.content.trim(), true); - }, - hydrateComponent: async (renderer, errorHandler) => { +export const dropdownPlugin: Plugin = { + ...flaggableJsonPlugin(pluginName, className), + hydrateComponent: async (renderer, errorHandler, specs) => { const dropdownInstances: DropdownInstance[] = []; - 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 (let index = 0; index < specs.length; index++) { + const specReview = specs[index]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(`#${specReview.containerId}`); - const spec: DropdownSpec = jsonObj; + const spec: DropdownSpec = specReview.approvedSpec; const html = `
diff --git a/packages/markdown/src/plugins/image.ts b/packages/markdown/src/plugins/image.ts index 33d8dce5..a68207be 100644 --- a/packages/markdown/src/plugins/image.ts +++ b/packages/markdown/src/plugins/image.ts @@ -7,6 +7,7 @@ import { ImageElementProps } from 'schema'; import { definePlugin, IInstance, Plugin } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { getJsonScriptTag, pluginClassName } from './util.js'; +import { flaggableJsonPlugin } from './config.js'; export interface ImageSpec extends ImageElementProps { srcSignalName: string; @@ -28,20 +29,18 @@ enum ImageOpacity { const pluginName = 'image'; const className = pluginClassName(pluginName); -export const imagePlugin: Plugin = { - name: pluginName, - initializePlugin: (md) => definePlugin(md, pluginName), - fence: token => { - return sanitizedHTML('div', { class: className }, token.content.trim(), true); - }, - hydrateComponent: async (renderer, errorHandler) => { +export const imagePlugin: Plugin = { + ...flaggableJsonPlugin(pluginName, className), + hydrateComponent: async (renderer, errorHandler, specs) => { const imageInstances: ImageInstance[] = []; - 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 (let index = 0; index < specs.length; index++) { + const specReview = specs[index]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(`#${specReview.containerId}`); - const spec: ImageSpec = jsonObj; + const spec: ImageSpec = specReview.approvedSpec; const element = document.createElement('img'); const spinner = document.createElement('div'); spinner.innerHTML = ` diff --git a/packages/markdown/src/plugins/presets.ts b/packages/markdown/src/plugins/presets.ts index 6f95f633..113b4dd1 100644 --- a/packages/markdown/src/plugins/presets.ts +++ b/packages/markdown/src/plugins/presets.ts @@ -7,6 +7,7 @@ import { Preset } from 'schema'; import { Batch, definePlugin, IInstance, Plugin, PrioritizedSignal } from '../factory.js'; import { sanitizedHTML } from '../sanitize.js'; import { getJsonScriptTag, pluginClassName } from './util.js'; +import { flaggableJsonPlugin } from './config.js'; export type PresetsSpec = Preset[]; @@ -19,21 +20,19 @@ interface PresetsInstance { const pluginName = 'presets'; const className = pluginClassName(pluginName); -export const presetsPlugin: Plugin = { - name: pluginName, - initializePlugin: (md) => definePlugin(md, pluginName), - fence: token => { - return sanitizedHTML('div', { class: className }, token.content.trim(), true); - }, - hydrateComponent: async (renderer, errorHandler) => { +export const presetsPlugin: Plugin = { + ...flaggableJsonPlugin(pluginName, className), + hydrateComponent: async (renderer, errorHandler, specs) => { const presetsInstances: PresetsInstance[] = []; - 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 (let index = 0; index < specs.length; index++) { + const specReview = specs[index]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(`#${specReview.containerId}`); const id = `${pluginName}-${index}`; - const presets = jsonObj as Preset[]; + const presets = specReview.approvedSpec; if (!Array.isArray(presets)) { container.innerHTML = '
Expected an array of presets
'; continue; diff --git a/packages/markdown/src/plugins/tabulator.ts b/packages/markdown/src/plugins/tabulator.ts index d069c528..95cb717a 100644 --- a/packages/markdown/src/plugins/tabulator.ts +++ b/packages/markdown/src/plugins/tabulator.ts @@ -3,12 +3,11 @@ * Licensed under the MIT License. */ -import { defaultCommonOptions } from 'common'; -import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; -import { sanitizedHTML } from '../sanitize.js'; +import { Batch, IInstance, Plugin, RawFlaggableSpec } from '../factory.js'; import { Tabulator as TabulatorType, Options as TabulatorOptions } from 'tabulator-tables'; import { getJsonScriptTag, pluginClassName } from './util.js'; import { TableElementProps } from 'schema'; +import { flaggableJsonPlugin } from './config.js'; interface TabulatorInstance { id: string; @@ -23,28 +22,34 @@ export interface TabulatorSpec extends TableElementProps { declare const Tabulator: typeof TabulatorType; +export function inspectTabulatorSpec(spec: TabulatorSpec) { + //TODO inspect spec for flags, such as http:// instead of https://, or other security issues + const flaggableSpec: RawFlaggableSpec = { + spec, + }; + return flaggableSpec; +} + const pluginName = 'tabulator'; const className = pluginClassName(pluginName); -export const tabulatorPlugin: Plugin = { - name: pluginName, - initializePlugin: (md) => definePlugin(md, pluginName), - fence: token => { - return sanitizedHTML('div', { class: className, style: 'box-sizing: border-box;' }, token.content.trim(), true); - }, - hydrateComponent: async (renderer, errorHandler) => { +export const tabulatorPlugin: Plugin = { + ...flaggableJsonPlugin(pluginName, className, inspectTabulatorSpec, { style: 'box-sizing: border-box;' }), + hydrateComponent: async (renderer, errorHandler, specs) => { const tabulatorInstances: TabulatorInstance[] = []; - 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 (let index = 0; index < specs.length; index++) { + const specReview = specs[index]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(`#${specReview.containerId}`); if (!Tabulator && index === 0) { errorHandler(new Error('Tabulator not found'), pluginName, index, 'init', container); continue; } - const spec: TabulatorSpec = jsonObj; + const spec: TabulatorSpec = specReview.approvedSpec; if (!spec.input_dataSourceName || !spec.output_dataSourceName) { errorHandler(new Error('Tabulator requires input_dataSourceName and output_dataSourceName'), pluginName, index, 'init', container); diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index f8608d3d..01573743 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -42,7 +42,7 @@ export function inspectVegaSpec(spec: Spec) { export const vegaPlugin: Plugin = { ...flaggableJsonPlugin(pluginName, className, inspectVegaSpec), - hydrateComponent: async (renderer, errorHandler, configContainers) => { + hydrateComponent: async (renderer, errorHandler, specs) => { //initialize the expressionFunction only once if (!expressionsInitialized) { expressionFunction('urlParam', urlParam); @@ -51,13 +51,13 @@ export const vegaPlugin: Plugin = { const vegaInstances: VegaInstance[] = []; const specInits: SpecInit[] = []; - for (let index = 0; index < configContainers.length; index++) { - const configContainer = configContainers[index]; - if (!configContainer.approvedSpec) { + for (let index = 0; index < specs.length; index++) { + const specReview = specs[index]; + if (!specReview.approvedSpec) { continue; } - const container = renderer.element.querySelector(`#${configContainer.containerId}`); - const specInit = createSpecInit(container, index, configContainer.approvedSpec); + const container = renderer.element.querySelector(`#${specReview.containerId}`); + const specInit = createSpecInit(container, index, specReview.approvedSpec); if (specInit) { specInits.push(specInit); } From 93ab5144328b55178c17740dd2290d258d5d49e4 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 14:27:04 -0700 Subject: [PATCH 51/61] slider & textbox plugins --- docs/schema/idoc_v1.d.ts | 8 +- docs/schema/idoc_v1.json | 4 + packages/compiler/src/md.ts | 46 +++------ packages/markdown/src/plugins/checkbox.ts | 4 +- packages/markdown/src/plugins/dropdown.ts | 5 +- packages/markdown/src/plugins/image.ts | 5 +- packages/markdown/src/plugins/index.ts | 4 + packages/markdown/src/plugins/interfaces.ts | 2 + packages/markdown/src/plugins/presets.ts | 5 +- packages/markdown/src/plugins/slider.ts | 107 ++++++++++++++++++++ packages/markdown/src/plugins/tabulator.ts | 2 +- packages/markdown/src/plugins/textbox.ts | 106 +++++++++++++++++++ packages/schema/src/interactive.ts | 6 +- 13 files changed, 259 insertions(+), 45 deletions(-) create mode 100644 packages/markdown/src/plugins/slider.ts create mode 100644 packages/markdown/src/plugins/textbox.ts diff --git a/docs/schema/idoc_v1.d.ts b/docs/schema/idoc_v1.d.ts index 3b3f58c8..09e97241 100644 --- a/docs/schema/idoc_v1.d.ts +++ b/docs/schema/idoc_v1.d.ts @@ -108,9 +108,13 @@ interface CheckboxElement extends VariableControl { * Textbox * use sparingly - typically only for text input */ -interface TextboxElement extends VariableControl { +interface TextboxElement extends VariableControl, TextboxElementProps { type: 'textbox'; } +interface TextboxElementProps { + /** whether to render as a textarea instead of input */ + multiline?: boolean; +} /** * Slider * prefer sliders over textbox for numbers. Never use for boolean values. @@ -247,4 +251,4 @@ type PageElement = MarkdownElement | InteractiveElement; type InteractiveDocumentWithSchema = InteractiveDocument & { $schema?: string; }; -export type { Calculation, ChartElement, ChartFull, ChartPlaceholder, ChartValue, CheckboxElement, DataLoader, DataLoaderBySpec, DataSource, DataSourceBase, DataSourceBaseFormat, DataSourceByDynamicURL, DataSourceByFile, DataSourceByJSON, DropdownElement, DropdownElementProps, DynamicDropdownOptions, ElementBase, ElementGroup, ImageElement, ImageElementProps, InteractiveDocument, InteractiveDocumentWithSchema, InteractiveElement, Layout, MappedNameValuePairs, MarkdownElement, NameValuePairs, PageElement, Preset, PresetsElement, PresetsElementProps, ReturnType, SliderElement, SliderElementProps, TableElement, TableElementProps, TextboxElement, UrlRef, Variable, VariableControl, VariableID, VariableType, VariableValue, VariableValueArray, VariableValuePrimitive }; \ No newline at end of file +export type { Calculation, ChartElement, ChartFull, ChartPlaceholder, ChartValue, CheckboxElement, DataLoader, DataLoaderBySpec, DataSource, DataSourceBase, DataSourceBaseFormat, DataSourceByDynamicURL, DataSourceByFile, DataSourceByJSON, DropdownElement, DropdownElementProps, DynamicDropdownOptions, ElementBase, ElementGroup, ImageElement, ImageElementProps, InteractiveDocument, InteractiveDocumentWithSchema, InteractiveElement, Layout, MappedNameValuePairs, MarkdownElement, NameValuePairs, PageElement, Preset, PresetsElement, PresetsElementProps, ReturnType, SliderElement, SliderElementProps, TableElement, TableElementProps, TextboxElement, TextboxElementProps, UrlRef, Variable, VariableControl, VariableID, VariableType, VariableValue, VariableValueArray, VariableValuePrimitive }; \ No newline at end of file diff --git a/docs/schema/idoc_v1.json b/docs/schema/idoc_v1.json index 59b8331b..102cbeca 100644 --- a/docs/schema/idoc_v1.json +++ b/docs/schema/idoc_v1.json @@ -2850,6 +2850,10 @@ "description": "optional label if the variableId is not descriptive enough", "type": "string" }, + "multiline": { + "description": "whether to render as a textarea instead of input", + "type": "boolean" + }, "type": { "const": "textbox", "type": "string" diff --git a/packages/compiler/src/md.ts b/packages/compiler/src/md.ts index 18e10024..aff66c95 100644 --- a/packages/compiler/src/md.ts +++ b/packages/compiler/src/md.ts @@ -1,6 +1,6 @@ import { Spec as VegaSpec } from 'vega-typings'; import { TopLevelSpec as VegaLiteSpec } from "vega-lite"; -import { ChartFull, DataSource, ElementGroup, InteractiveDocument, TableElement, Variable } from 'schema'; +import { ChartFull, DataSource, ElementGroup, InteractiveDocument, TableElement, TextboxElement, Variable } from 'schema'; import { getChartType } from './util.js'; import { addDynamicDataLoaderToSpec, addStaticDataLoaderToSpec } from './loader.js'; import { Plugins } from '@microsoft/interactive-document-markdown'; @@ -149,23 +149,15 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve break; } case 'slider': { - const spec: VegaSpec = { - $schema, - signals: [ - { - name: element.variableId, - value: variables.find(v => v.variableId === element.variableId)?.initialValue, - bind: { - input: "range", - min: element.min, - max: element.max, - step: element.step, - debounce: 100, - } - } - ] + const sliderSpec: Plugins.SliderSpec = { + variableId: element.variableId, + value: variables.find(v => v.variableId === element.variableId)?.initialValue as number, + label: element.label, + min: element.min, + max: element.max, + step: element.step, }; - mdElements.push(chartWrap(spec)); + mdElements.push(jsonWrap('slider', JSON.stringify(sliderSpec, null, 2))); break; } case 'table': { @@ -175,20 +167,14 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve break; } case 'textbox': { - const spec: VegaSpec = { - $schema, - signals: [ - { - name: element.variableId, - value: variables.find(v => v.variableId === element.variableId)?.initialValue, - bind: { - input: "text", - debounce: 100, - } - } - ] + const textboxElement = element as TextboxElement; + const textboxSpec: Plugins.TextboxSpec = { + variableId: textboxElement.variableId, + value: variables.find(v => v.variableId === textboxElement.variableId)?.initialValue as string, + label: textboxElement.label, + multiline: textboxElement.multiline, }; - mdElements.push(chartWrap(spec)); + mdElements.push(jsonWrap('textbox', JSON.stringify(textboxSpec, null, 2))); break; } } diff --git a/packages/markdown/src/plugins/checkbox.ts b/packages/markdown/src/plugins/checkbox.ts index 4def01dc..0b2444cc 100644 --- a/packages/markdown/src/plugins/checkbox.ts +++ b/packages/markdown/src/plugins/checkbox.ts @@ -5,7 +5,7 @@ import { VariableControl } from 'schema'; import { Batch, IInstance, Plugin } from '../factory.js'; -import { getJsonScriptTag, pluginClassName } from './util.js'; +import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; interface CheckboxInstance { @@ -38,7 +38,7 @@ export const checkboxPlugin: Plugin = {
`; diff --git a/packages/markdown/src/plugins/dropdown.ts b/packages/markdown/src/plugins/dropdown.ts index 5c0083c5..cb98d5af 100644 --- a/packages/markdown/src/plugins/dropdown.ts +++ b/packages/markdown/src/plugins/dropdown.ts @@ -4,9 +4,8 @@ */ import { DropdownElementProps } from 'schema'; -import { Batch, definePlugin, IInstance, Plugin } from '../factory.js'; -import { sanitizedHTML } from '../sanitize.js'; -import { getJsonScriptTag, pluginClassName } from './util.js'; +import { Batch, IInstance, Plugin } from '../factory.js'; +import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; interface DropdownInstance { diff --git a/packages/markdown/src/plugins/image.ts b/packages/markdown/src/plugins/image.ts index a68207be..fb57f275 100644 --- a/packages/markdown/src/plugins/image.ts +++ b/packages/markdown/src/plugins/image.ts @@ -4,9 +4,8 @@ */ import { ImageElementProps } from 'schema'; -import { definePlugin, IInstance, Plugin } from '../factory.js'; -import { sanitizedHTML } from '../sanitize.js'; -import { getJsonScriptTag, pluginClassName } from './util.js'; +import { IInstance, Plugin } from '../factory.js'; +import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; export interface ImageSpec extends ImageElementProps { diff --git a/packages/markdown/src/plugins/index.ts b/packages/markdown/src/plugins/index.ts index c947d2c4..db5657d1 100644 --- a/packages/markdown/src/plugins/index.ts +++ b/packages/markdown/src/plugins/index.ts @@ -11,7 +11,9 @@ import { dropdownPlugin } from './dropdown.js'; import { imagePlugin } from './image.js'; import { placeholdersPlugin } from './placeholders.js'; import { presetsPlugin } from './presets.js'; +import { sliderPlugin } from './slider.js'; import { tabulatorPlugin } from './tabulator.js'; +import { textboxPlugin } from './textbox.js'; import { vegaLitePlugin } from './vega-lite.js'; import { vegaPlugin } from './vega.js'; @@ -22,7 +24,9 @@ export function registerNativePlugins() { registerMarkdownPlugin(imagePlugin); registerMarkdownPlugin(placeholdersPlugin); registerMarkdownPlugin(presetsPlugin); + registerMarkdownPlugin(sliderPlugin); registerMarkdownPlugin(tabulatorPlugin); + registerMarkdownPlugin(textboxPlugin); registerMarkdownPlugin(vegaLitePlugin); registerMarkdownPlugin(vegaPlugin); } diff --git a/packages/markdown/src/plugins/interfaces.ts b/packages/markdown/src/plugins/interfaces.ts index fb869fba..916ab983 100644 --- a/packages/markdown/src/plugins/interfaces.ts +++ b/packages/markdown/src/plugins/interfaces.ts @@ -2,4 +2,6 @@ export { CheckboxSpec } from './checkbox.js'; export { DropdownSpec } from './dropdown.js'; export { ImageSpec } from './image.js'; export { PresetsSpec } from './presets.js'; +export { SliderSpec } from './slider.js'; export { TabulatorSpec } from './tabulator.js'; +export { TextboxSpec } from './textbox.js'; diff --git a/packages/markdown/src/plugins/presets.ts b/packages/markdown/src/plugins/presets.ts index 113b4dd1..6549830c 100644 --- a/packages/markdown/src/plugins/presets.ts +++ b/packages/markdown/src/plugins/presets.ts @@ -4,9 +4,8 @@ */ import { Preset } from 'schema'; -import { Batch, definePlugin, IInstance, Plugin, PrioritizedSignal } from '../factory.js'; -import { sanitizedHTML } from '../sanitize.js'; -import { getJsonScriptTag, pluginClassName } from './util.js'; +import { Batch, IInstance, Plugin, PrioritizedSignal } from '../factory.js'; +import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; export type PresetsSpec = Preset[]; diff --git a/packages/markdown/src/plugins/slider.ts b/packages/markdown/src/plugins/slider.ts new file mode 100644 index 00000000..a572b257 --- /dev/null +++ b/packages/markdown/src/plugins/slider.ts @@ -0,0 +1,107 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { VariableControl, SliderElementProps } from 'schema'; +import { Batch, IInstance, Plugin } from '../factory.js'; +import { pluginClassName } from './util.js'; +import { flaggableJsonPlugin } from './config.js'; + +interface SliderInstance { + id: string; + spec: SliderSpec; + element: HTMLInputElement; +} + +export interface SliderSpec extends VariableControl, SliderElementProps { + value?: number; +} + +const pluginName = 'slider'; +const className = pluginClassName(pluginName); + +export const sliderPlugin: Plugin = { + ...flaggableJsonPlugin(pluginName, className), + hydrateComponent: async (renderer, errorHandler, specs) => { + const sliderInstances: SliderInstance[] = []; + for (let index = 0; index < specs.length; index++) { + const specReview = specs[index]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(`#${specReview.containerId}`); + + const spec: SliderSpec = specReview.approvedSpec; + + const html = `
+
+ +
+
`; + container.innerHTML = html; + const element = container.querySelector('input[type="range"]') as HTMLInputElement; + + const sliderInstance: SliderInstance = { id: `${pluginName}-${index}`, spec, element }; + sliderInstances.push(sliderInstance); + } + + const instances: IInstance[] = sliderInstances.map((sliderInstance) => { + const { element, spec } = sliderInstance; + const valueSpan = element.parentElement?.querySelector('.vega-bind-value') as HTMLSpanElement; + + const initialSignals = [{ + name: spec.variableId, + value: spec.value || spec.min, + priority: 1, + isData: false, + }]; + + return { + ...sliderInstance, + initialSignals, + recieveBatch: async (batch) => { + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value as number; + element.value = value.toString(); + if (valueSpan) { + valueSpan.textContent = value.toString(); + } + } + }, + beginListening() { + // Wire up handler to send the slider value to the signal bus + const updateValue = (e: Event) => { + const value = parseFloat((e.target as HTMLInputElement).value); + if (valueSpan) { + valueSpan.textContent = value.toString(); + } + const batch: Batch = { + [spec.variableId]: { + value, + isData: false, + }, + }; + renderer.signalBus.broadcast(sliderInstance.id, batch); + }; + + element.addEventListener('input', updateValue); + element.addEventListener('change', updateValue); + }, + getCurrentSignalValue: () => { + return parseFloat(element.value); + }, + destroy: () => { + element.removeEventListener('input', sliderInstance.element.oninput as EventListener); + element.removeEventListener('change', sliderInstance.element.onchange as EventListener); + }, + }; + }); + return instances; + }, +}; diff --git a/packages/markdown/src/plugins/tabulator.ts b/packages/markdown/src/plugins/tabulator.ts index 95cb717a..2376840f 100644 --- a/packages/markdown/src/plugins/tabulator.ts +++ b/packages/markdown/src/plugins/tabulator.ts @@ -5,7 +5,7 @@ import { Batch, IInstance, Plugin, RawFlaggableSpec } from '../factory.js'; import { Tabulator as TabulatorType, Options as TabulatorOptions } from 'tabulator-tables'; -import { getJsonScriptTag, pluginClassName } from './util.js'; +import { pluginClassName } from './util.js'; import { TableElementProps } from 'schema'; import { flaggableJsonPlugin } from './config.js'; diff --git a/packages/markdown/src/plugins/textbox.ts b/packages/markdown/src/plugins/textbox.ts new file mode 100644 index 00000000..5acec9a2 --- /dev/null +++ b/packages/markdown/src/plugins/textbox.ts @@ -0,0 +1,106 @@ +/*! +* Copyright (c) Microsoft Corporation. +* Licensed under the MIT License. +*/ + +import { VariableControl } from 'schema'; +import { Batch, IInstance, Plugin } from '../factory.js'; +import { pluginClassName } from './util.js'; +import { flaggableJsonPlugin } from './config.js'; + +interface TextboxInstance { + id: string; + spec: TextboxSpec; + element: HTMLInputElement | HTMLTextAreaElement; +} + +export interface TextboxSpec extends VariableControl { + value?: string; + /** whether to render as a textarea instead of input */ + multiline?: boolean; + /** placeholder text to show when input is empty */ + placeholder?: string; +} + +const pluginName = 'textbox'; +const className = pluginClassName(pluginName); + +export const textboxPlugin: Plugin = { + ...flaggableJsonPlugin(pluginName, className), + hydrateComponent: async (renderer, errorHandler, specs) => { + const textboxInstances: TextboxInstance[] = []; + for (let index = 0; index < specs.length; index++) { + const specReview = specs[index]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(`#${specReview.containerId}`); + + const spec: TextboxSpec = specReview.approvedSpec; + + const placeholderAttr = spec.placeholder ? ` placeholder="${spec.placeholder}"` : ''; + const inputElement = spec.multiline + ? `` + : ``; + + const html = `
+
+ +
+
`; + container.innerHTML = html; + const element = container.querySelector(spec.multiline ? 'textarea' : 'input[type="text"]') as HTMLInputElement | HTMLTextAreaElement; + + const textboxInstance: TextboxInstance = { id: `${pluginName}-${index}`, spec, element }; + textboxInstances.push(textboxInstance); + } + + const instances: IInstance[] = textboxInstances.map((textboxInstance) => { + const { element, spec } = textboxInstance; + const initialSignals = [{ + name: spec.variableId, + value: spec.value || '', + priority: 1, + isData: false, + }]; + + return { + ...textboxInstance, + initialSignals, + recieveBatch: async (batch) => { + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value as string; + element.value = value; + } + }, + beginListening() { + // Wire up handler to send the text value to the signal bus + const updateValue = (e: Event) => { + const value = (e.target as HTMLInputElement | HTMLTextAreaElement).value; + const batch: Batch = { + [spec.variableId]: { + value, + isData: false, + }, + }; + renderer.signalBus.broadcast(textboxInstance.id, batch); + }; + + element.addEventListener('input', updateValue); + element.addEventListener('change', updateValue); + }, + getCurrentSignalValue: () => { + return element.value; + }, + destroy: () => { + element.removeEventListener('input', textboxInstance.element.oninput as EventListener); + element.removeEventListener('change', textboxInstance.element.onchange as EventListener); + }, + }; + }); + return instances; + }, +}; diff --git a/packages/schema/src/interactive.ts b/packages/schema/src/interactive.ts index 2480c68a..bba91281 100644 --- a/packages/schema/src/interactive.ts +++ b/packages/schema/src/interactive.ts @@ -16,9 +16,13 @@ export interface CheckboxElement extends VariableControl { * Textbox * use sparingly - typically only for text input */ -export interface TextboxElement extends VariableControl { +export interface TextboxElement extends VariableControl, TextboxElementProps { type: 'textbox'; } +export interface TextboxElementProps { + /** whether to render as a textarea instead of input */ + multiline?: boolean; +} /** * Slider From c8bb872464d683ba018896d13c2abab18649bc61 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 17:38:54 -0700 Subject: [PATCH 52/61] normalize elementProps --- docs/schema/idoc_v1.d.ts | 28 +++++++++------- docs/schema/idoc_v1.json | 27 ++++++++++----- packages/compiler/src/md.ts | 4 +-- packages/compiler/src/spec.ts | 6 ++-- packages/editor/src/app.tsx | 6 ++-- packages/markdown/src/plugins/tabulator.ts | 18 +++++----- packages/markdown/src/plugins/textbox.ts | 8 ++--- packages/schema/src/interactive.ts | 38 +++++++++++++--------- 8 files changed, 75 insertions(+), 60 deletions(-) diff --git a/docs/schema/idoc_v1.d.ts b/docs/schema/idoc_v1.d.ts index 09e97241..e2ef81a3 100644 --- a/docs/schema/idoc_v1.d.ts +++ b/docs/schema/idoc_v1.d.ts @@ -101,28 +101,32 @@ type DataLoader = DataSource | DataLoaderBySpec; * Checkbox * use for boolean values */ -interface CheckboxElement extends VariableControl { +interface CheckboxElement extends CheckboxProps { type: 'checkbox'; } +interface CheckboxProps extends VariableControl { +} /** * Textbox * use sparingly - typically only for text input */ -interface TextboxElement extends VariableControl, TextboxElementProps { +interface TextboxElement extends TextboxElementProps { type: 'textbox'; } -interface TextboxElementProps { +interface TextboxElementProps extends VariableControl { /** whether to render as a textarea instead of input */ multiline?: boolean; + /** placeholder text to show when input is empty */ + placeholder?: string; } /** * Slider * prefer sliders over textbox for numbers. Never use for boolean values. */ -interface SliderElement extends VariableControl, SliderElementProps { +interface SliderElement extends SliderElementProps { type: 'slider'; } -interface SliderElementProps { +interface SliderElementProps extends VariableControl { min: number; max: number; step: number; @@ -205,14 +209,14 @@ interface Preset { * Table * use for tabular data */ -interface TableElement extends ElementBase, TableElementProps { +interface TableElement extends TableElementProps { type: 'table'; } -interface TableElementProps { - input_dataSourceName: string; - output_dataSourceName: string; - /** Tabulator options (must be serializable, so no callbacks allowed) */ - options?: object; +interface TableElementProps extends VariableControl { + /** Name of the data source to use for incoming data (output data is available via the variableId of this table element) */ + dataSourceName: string; + /** Tabulator options (must be JSON stringify-able, so no callbacks allowed) */ + tabulatorOptions?: object; } /** * Union type for all possible interactive elements @@ -251,4 +255,4 @@ type PageElement = MarkdownElement | InteractiveElement; type InteractiveDocumentWithSchema = InteractiveDocument & { $schema?: string; }; -export type { Calculation, ChartElement, ChartFull, ChartPlaceholder, ChartValue, CheckboxElement, DataLoader, DataLoaderBySpec, DataSource, DataSourceBase, DataSourceBaseFormat, DataSourceByDynamicURL, DataSourceByFile, DataSourceByJSON, DropdownElement, DropdownElementProps, DynamicDropdownOptions, ElementBase, ElementGroup, ImageElement, ImageElementProps, InteractiveDocument, InteractiveDocumentWithSchema, InteractiveElement, Layout, MappedNameValuePairs, MarkdownElement, NameValuePairs, PageElement, Preset, PresetsElement, PresetsElementProps, ReturnType, SliderElement, SliderElementProps, TableElement, TableElementProps, TextboxElement, TextboxElementProps, UrlRef, Variable, VariableControl, VariableID, VariableType, VariableValue, VariableValueArray, VariableValuePrimitive }; \ No newline at end of file +export type { Calculation, ChartElement, ChartFull, ChartPlaceholder, ChartValue, CheckboxElement, CheckboxProps, DataLoader, DataLoaderBySpec, DataSource, DataSourceBase, DataSourceBaseFormat, DataSourceByDynamicURL, DataSourceByFile, DataSourceByJSON, DropdownElement, DropdownElementProps, DynamicDropdownOptions, ElementBase, ElementGroup, ImageElement, ImageElementProps, InteractiveDocument, InteractiveDocumentWithSchema, InteractiveElement, Layout, MappedNameValuePairs, MarkdownElement, NameValuePairs, PageElement, Preset, PresetsElement, PresetsElementProps, ReturnType, SliderElement, SliderElementProps, TableElement, TableElementProps, TextboxElement, TextboxElementProps, UrlRef, Variable, VariableControl, VariableID, VariableType, VariableValue, VariableValueArray, VariableValuePrimitive }; \ No newline at end of file diff --git a/docs/schema/idoc_v1.json b/docs/schema/idoc_v1.json index 102cbeca..b012e28c 100644 --- a/docs/schema/idoc_v1.json +++ b/docs/schema/idoc_v1.json @@ -2820,25 +2820,30 @@ "additionalProperties": false, "description": "Table use for tabular data", "properties": { - "input_dataSourceName": { + "dataSourceName": { + "description": "Name of the data source to use for incoming data (output data is available via the variableId of this table element)", "type": "string" }, - "options": { - "description": "Tabulator options (must be serializable, so no callbacks allowed)", - "type": "object" - }, - "output_dataSourceName": { + "label": { + "description": "optional label if the variableId is not descriptive enough", "type": "string" }, + "tabulatorOptions": { + "description": "Tabulator options (must be JSON stringify-able, so no callbacks allowed)", + "type": "object" + }, "type": { "const": "table", "type": "string" + }, + "variableId": { + "$ref": "#/definitions/VariableID" } }, "required": [ - "input_dataSourceName", - "output_dataSourceName", - "type" + "dataSourceName", + "type", + "variableId" ], "type": "object" }, @@ -2854,6 +2859,10 @@ "description": "whether to render as a textarea instead of input", "type": "boolean" }, + "placeholder": { + "description": "placeholder text to show when input is empty", + "type": "string" + }, "type": { "const": "textbox", "type": "string" diff --git a/packages/compiler/src/md.ts b/packages/compiler/src/md.ts index aff66c95..05eda725 100644 --- a/packages/compiler/src/md.ts +++ b/packages/compiler/src/md.ts @@ -161,8 +161,8 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve break; } case 'table': { - const { input_dataSourceName, output_dataSourceName, options } = element; - const tableSpec: Plugins.TabulatorSpec = { input_dataSourceName, output_dataSourceName, options }; + const { dataSourceName, variableId, tabulatorOptions } = element; + const tableSpec: Plugins.TabulatorSpec = { dataSourceName, variableId, tabulatorOptions }; mdElements.push(jsonWrap('tabulator', JSON.stringify(tableSpec, null, 2))); break; } diff --git a/packages/compiler/src/spec.ts b/packages/compiler/src/spec.ts index 8146bc80..f16b1f5a 100644 --- a/packages/compiler/src/spec.ts +++ b/packages/compiler/src/spec.ts @@ -16,10 +16,10 @@ export function createSpecWithVariables(variables: Variable[], tableElements: Ta //add table elements as data sources tableElements.forEach((table) => { - const { output_dataSourceName } = table; - spec.signals.push(dataAsSignal(output_dataSourceName)); + const { variableId } = table; + spec.signals.push(dataAsSignal(variableId)); spec.data.unshift({ - name: output_dataSourceName, + name: variableId, values: [], }); }); diff --git a/packages/editor/src/app.tsx b/packages/editor/src/app.tsx index 7d5cc0b0..d4c95ea3 100644 --- a/packages/editor/src/app.tsx +++ b/packages/editor/src/app.tsx @@ -177,9 +177,9 @@ const initialPage: InteractiveDocument = { "# Seattle Weather\n\nData table:", { "type": "table", - "input_dataSourceName": "seattle_weather", - "output_dataSourceName": "seattle_weather_selected", - "options": {} + "dataSourceName": "seattle_weather", + "variableId": "seattle_weather_selected", + "tabulatorOptions": {} }, "Here is a stacked bar chart of Seattle weather:\nEach bar represents the count of weather types for each month.\nThe colors distinguish between different weather conditions such as sun, fog, drizzle, rain, and snow.", { diff --git a/packages/markdown/src/plugins/tabulator.ts b/packages/markdown/src/plugins/tabulator.ts index 2376840f..3d6cd9f0 100644 --- a/packages/markdown/src/plugins/tabulator.ts +++ b/packages/markdown/src/plugins/tabulator.ts @@ -51,11 +51,11 @@ export const tabulatorPlugin: Plugin = { const spec: TabulatorSpec = specReview.approvedSpec; - if (!spec.input_dataSourceName || !spec.output_dataSourceName) { - errorHandler(new Error('Tabulator requires input_dataSourceName and output_dataSourceName'), pluginName, index, 'init', container); + if (!spec.dataSourceName || !spec.variableId) { + errorHandler(new Error('Tabulator requires dataSourceName and variableId'), pluginName, index, 'init', container); continue; - } else if (spec.input_dataSourceName === spec.output_dataSourceName) { - errorHandler(new Error('Tabulator input_dataSourceName and output_dataSourceName cannot be the same'), pluginName, index, 'init', container); + } else if (spec.dataSourceName === spec.variableId) { + errorHandler(new Error('Tabulator dataSourceName and variableId cannot be the same'), pluginName, index, 'init', container); continue; } @@ -80,14 +80,14 @@ export const tabulatorPlugin: Plugin = { } const instances: IInstance[] = tabulatorInstances.map((tabulatorInstance, index) => { const initialSignals = [{ - name: tabulatorInstance.spec.input_dataSourceName, + name: tabulatorInstance.spec.dataSourceName, value: null, priority: -1, isData: true, }]; if (tabulatorInstance.spec.options?.selectableRows) { initialSignals.push({ - name: tabulatorInstance.spec.output_dataSourceName, + name: tabulatorInstance.spec.variableId, value: [], priority: -1, isData: true, @@ -97,7 +97,7 @@ export const tabulatorPlugin: Plugin = { ...tabulatorInstance, initialSignals, recieveBatch: async (batch) => { - const newData = batch[tabulatorInstance.spec.input_dataSourceName]?.value as object[]; + const newData = batch[tabulatorInstance.spec.dataSourceName]?.value as object[]; if (newData) { //make sure tabulator is ready before setting data if (!tabulatorInstance.built) { @@ -116,12 +116,12 @@ export const tabulatorPlugin: Plugin = { if (tabulatorInstance.spec.options?.selectableRows) { for (const { isData, signalName } of sharedSignals) { if (isData) { - const matchData = signalName === tabulatorInstance.spec.output_dataSourceName; + const matchData = signalName === tabulatorInstance.spec.variableId; if (matchData) { tabulatorInstance.table.on('rowSelectionChanged', (e, rows) => { const selectedData = tabulatorInstance.table.getSelectedData(); const batch: Batch = { - [tabulatorInstance.spec.output_dataSourceName]: { + [tabulatorInstance.spec.variableId]: { value: selectedData, isData: true, }, diff --git a/packages/markdown/src/plugins/textbox.ts b/packages/markdown/src/plugins/textbox.ts index 5acec9a2..6f454481 100644 --- a/packages/markdown/src/plugins/textbox.ts +++ b/packages/markdown/src/plugins/textbox.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { VariableControl } from 'schema'; +import { TextboxElementProps } from 'schema'; import { Batch, IInstance, Plugin } from '../factory.js'; import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; @@ -14,12 +14,8 @@ interface TextboxInstance { element: HTMLInputElement | HTMLTextAreaElement; } -export interface TextboxSpec extends VariableControl { +export interface TextboxSpec extends TextboxElementProps { value?: string; - /** whether to render as a textarea instead of input */ - multiline?: boolean; - /** placeholder text to show when input is empty */ - placeholder?: string; } const pluginName = 'textbox'; diff --git a/packages/schema/src/interactive.ts b/packages/schema/src/interactive.ts index bba91281..85840666 100644 --- a/packages/schema/src/interactive.ts +++ b/packages/schema/src/interactive.ts @@ -8,30 +8,34 @@ import { DataSourceBase, VariableID, VariableControl, ElementBase, UrlRef } from * Checkbox * use for boolean values */ -export interface CheckboxElement extends VariableControl { +export interface CheckboxElement extends CheckboxProps { type: 'checkbox'; } +export interface CheckboxProps extends VariableControl { +} /** * Textbox * use sparingly - typically only for text input */ -export interface TextboxElement extends VariableControl, TextboxElementProps { +export interface TextboxElement extends TextboxElementProps { type: 'textbox'; } -export interface TextboxElementProps { +export interface TextboxElementProps extends VariableControl { /** whether to render as a textarea instead of input */ multiline?: boolean; + /** placeholder text to show when input is empty */ + placeholder?: string; } /** * Slider * prefer sliders over textbox for numbers. Never use for boolean values. */ -export interface SliderElement extends VariableControl, SliderElementProps { +export interface SliderElement extends SliderElementProps { type: 'slider'; } -export interface SliderElementProps { +export interface SliderElementProps extends VariableControl { min: number; max: number; step: number; @@ -108,30 +112,32 @@ export interface ImageElementProps { * use for storing and applying preset batches of signal states */ export interface PresetsElement extends ElementBase, PresetsElementProps { - type: 'presets'; + type: 'presets'; } export interface PresetsElementProps { - presets: Preset[]; + presets: Preset[]; } export interface Preset { - name: string; - description?: string; - state: { [signalName: string]: unknown }; + name: string; + description?: string; + state: { [signalName: string]: unknown }; } /** * Table * use for tabular data */ -export interface TableElement extends ElementBase, TableElementProps { +export interface TableElement extends TableElementProps { type: 'table'; } -export interface TableElementProps { - input_dataSourceName: string; - output_dataSourceName: string; - /** Tabulator options (must be serializable, so no callbacks allowed) */ - options?: object; +export interface TableElementProps extends VariableControl { + + /** Name of the data source to use for incoming data (output data is available via the variableId of this table element) */ + dataSourceName: string; + + /** Tabulator options (must be JSON stringify-able, so no callbacks allowed) */ + tabulatorOptions?: object; /** * Example table options From 27a2ee9480b555e0a2d8dab9dd069517e2763a50 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 17:56:51 -0700 Subject: [PATCH 53/61] fix tabulatorOptions --- packages/markdown/src/plugins/tabulator.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/markdown/src/plugins/tabulator.ts b/packages/markdown/src/plugins/tabulator.ts index 3d6cd9f0..daa21d34 100644 --- a/packages/markdown/src/plugins/tabulator.ts +++ b/packages/markdown/src/plugins/tabulator.ts @@ -17,7 +17,7 @@ interface TabulatorInstance { } export interface TabulatorSpec extends TableElementProps { - options?: TabulatorOptions; + tabulatorOptions?: TabulatorOptions; //recast the default with strong typing } declare const Tabulator: typeof TabulatorType; @@ -66,8 +66,8 @@ export const tabulatorPlugin: Plugin = { }; //see if default options is an object with no properties - if (spec.options && Object.keys(spec.options).length > 0) { - options = spec.options; + if (spec.tabulatorOptions && Object.keys(spec.tabulatorOptions).length > 0) { + options = spec.tabulatorOptions; } const table = new Tabulator(container as HTMLElement, options); @@ -85,7 +85,7 @@ export const tabulatorPlugin: Plugin = { priority: -1, isData: true, }]; - if (tabulatorInstance.spec.options?.selectableRows) { + if (tabulatorInstance.spec.tabulatorOptions?.selectableRows) { initialSignals.push({ name: tabulatorInstance.spec.variableId, value: [], @@ -113,7 +113,7 @@ export const tabulatorPlugin: Plugin = { } }, beginListening(sharedSignals) { - if (tabulatorInstance.spec.options?.selectableRows) { + if (tabulatorInstance.spec.tabulatorOptions?.selectableRows) { for (const { isData, signalName } of sharedSignals) { if (isData) { const matchData = signalName === tabulatorInstance.spec.variableId; From da50e83b2c0d7d92ca91d9425dc5e5210197f614 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 18:01:28 -0700 Subject: [PATCH 54/61] correct tavble schema --- docs/assets/examples/grocery-list.idoc.json | 3 ++- docs/assets/examples/seattle-weather/4.idoc.json | 2 +- docs/assets/examples/seattle-weather/5.idoc.json | 3 ++- docs/assets/examples/seattle-weather/6.idoc.json | 2 +- docs/assets/examples/seattle-weather/7.idoc.json | 2 +- docs/assets/examples/seattle-weather/8.idoc.json | 2 +- docs/assets/examples/seattle-weather/9.idoc.json | 2 +- docs/assets/examples/slides.idoc.json | 3 ++- 8 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/assets/examples/grocery-list.idoc.json b/docs/assets/examples/grocery-list.idoc.json index 40c63673..79e0ad51 100644 --- a/docs/assets/examples/grocery-list.idoc.json +++ b/docs/assets/examples/grocery-list.idoc.json @@ -69,8 +69,9 @@ "## Select the items you want to buy\n", { "type": "table", + "variableId": "itemsData_selected", "dataSourceName": "itemsData", - "options": { + "tabulatorOptions": { "autoColumns": true, "layout": "fitColumns", "minHeight": "200px", diff --git a/docs/assets/examples/seattle-weather/4.idoc.json b/docs/assets/examples/seattle-weather/4.idoc.json index 9e7c53ba..653bc93b 100644 --- a/docs/assets/examples/seattle-weather/4.idoc.json +++ b/docs/assets/examples/seattle-weather/4.idoc.json @@ -21,7 +21,7 @@ { "type": "table", "dataSourceName": "seattle_weather", - "options": {} + "variableId": "seattle_weather_selected" }, "Here is a stacked bar chart of Seattle weather:\nEach bar represents the count of weather types for each month.\nThe colors distinguish between different weather conditions such as sun, fog, drizzle, rain, and snow.", { diff --git a/docs/assets/examples/seattle-weather/5.idoc.json b/docs/assets/examples/seattle-weather/5.idoc.json index 04f1fb35..9777a12e 100644 --- a/docs/assets/examples/seattle-weather/5.idoc.json +++ b/docs/assets/examples/seattle-weather/5.idoc.json @@ -26,7 +26,8 @@ { "type": "table", "dataSourceName": "seattle_weather", - "options": {} + "variableId": "seattle_weather_selected", + "tabulatorOptions": {} }, "Here is a stacked bar chart of Seattle weather:\nEach bar represents the count of weather types for each month.\nThe colors distinguish between different weather conditions such as sun, fog, drizzle, rain, and snow.", { diff --git a/docs/assets/examples/seattle-weather/6.idoc.json b/docs/assets/examples/seattle-weather/6.idoc.json index f876b10a..a3ac113a 100644 --- a/docs/assets/examples/seattle-weather/6.idoc.json +++ b/docs/assets/examples/seattle-weather/6.idoc.json @@ -39,7 +39,7 @@ { "type": "table", "dataSourceName": "seattle_weather", - "options": {} + "variableId": "seattle_weather_selected" }, "Here is a stacked bar chart of Seattle weather:\nEach bar represents the count of weather types for each month.\nThe colors distinguish between different weather conditions such as sun, fog, drizzle, rain, and snow.", { diff --git a/docs/assets/examples/seattle-weather/7.idoc.json b/docs/assets/examples/seattle-weather/7.idoc.json index 08e5d444..47afc0eb 100644 --- a/docs/assets/examples/seattle-weather/7.idoc.json +++ b/docs/assets/examples/seattle-weather/7.idoc.json @@ -95,7 +95,7 @@ { "type": "table", "dataSourceName": "seattle_weather", - "options": {} + "variableId": "seattle_weather_selected" }, "Here is a stacked bar chart of Seattle weather:\nEach bar represents the count of weather types for each month.\nThe colors distinguish between different weather conditions such as sun, fog, drizzle, rain, and snow.", { diff --git a/docs/assets/examples/seattle-weather/8.idoc.json b/docs/assets/examples/seattle-weather/8.idoc.json index 325c40ec..73cd015e 100644 --- a/docs/assets/examples/seattle-weather/8.idoc.json +++ b/docs/assets/examples/seattle-weather/8.idoc.json @@ -125,7 +125,7 @@ { "type": "table", "dataSourceName": "seattle_weather", - "options": {} + "variableId": "seattle_weather_selected" }, "Here is a stacked bar chart of Seattle weather:\nEach bar represents the count of weather types for each month.\nThe colors distinguish between different weather conditions such as sun, fog, drizzle, rain, and snow.", { diff --git a/docs/assets/examples/seattle-weather/9.idoc.json b/docs/assets/examples/seattle-weather/9.idoc.json index fcc7a68b..ec1b2255 100644 --- a/docs/assets/examples/seattle-weather/9.idoc.json +++ b/docs/assets/examples/seattle-weather/9.idoc.json @@ -191,7 +191,7 @@ { "type": "table", "dataSourceName": "seattle_weather", - "options": {} + "variableId": "seattle_weather_selected" }, "Here is a stacked bar chart of Seattle weather:\nEach bar represents the count of weather types for each month.\nThe colors distinguish between different weather conditions such as sun, fog, drizzle, rain, and snow.", { diff --git a/docs/assets/examples/slides.idoc.json b/docs/assets/examples/slides.idoc.json index 62e0d5ff..2c98b92a 100644 --- a/docs/assets/examples/slides.idoc.json +++ b/docs/assets/examples/slides.idoc.json @@ -94,7 +94,8 @@ { "type": "table", "dataSourceName": "itemsData", - "options": { + "variableId": "itemsData_selected", + "tabulatorOptions": { "autoColumns": true, "layout": "fitColumns", "minHeight": "200px", From badc2d87990461b9863ccd5f8a65025da5c25c90 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 18:04:53 -0700 Subject: [PATCH 55/61] correct table schema --- docs/assets/examples/seattle-weather/4.idoc.md | 4 ++-- docs/assets/examples/seattle-weather/5.idoc.md | 4 ++-- docs/assets/examples/seattle-weather/6.idoc.md | 4 ++-- docs/assets/examples/seattle-weather/7.idoc.md | 4 ++-- docs/assets/examples/seattle-weather/8.idoc.md | 4 ++-- docs/assets/examples/seattle-weather/9.idoc.md | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/assets/examples/seattle-weather/4.idoc.md b/docs/assets/examples/seattle-weather/4.idoc.md index 3f3d945b..2cb7d6cf 100644 --- a/docs/assets/examples/seattle-weather/4.idoc.md +++ b/docs/assets/examples/seattle-weather/4.idoc.md @@ -26,8 +26,8 @@ Data table: ```json tabulator { - "dataSignalName": "seattle_weather", - "options": {} + "dataSourceName": "seattle_weather", + "variableId": "seattle_weather_selected" } ``` diff --git a/docs/assets/examples/seattle-weather/5.idoc.md b/docs/assets/examples/seattle-weather/5.idoc.md index d51c9e9a..5abfd5be 100644 --- a/docs/assets/examples/seattle-weather/5.idoc.md +++ b/docs/assets/examples/seattle-weather/5.idoc.md @@ -32,8 +32,8 @@ Data table: ```json tabulator { - "dataSignalName": "seattle_weather", - "options": {} + "dataSourceName": "seattle_weather", + "variableId": "seattle_weather_selected" } ``` diff --git a/docs/assets/examples/seattle-weather/6.idoc.md b/docs/assets/examples/seattle-weather/6.idoc.md index 01f8e547..9ce55a4b 100644 --- a/docs/assets/examples/seattle-weather/6.idoc.md +++ b/docs/assets/examples/seattle-weather/6.idoc.md @@ -40,8 +40,8 @@ Data table: ```json tabulator { - "dataSignalName": "seattle_weather", - "options": {} + "dataSourceName": "seattle_weather", + "variableId": "seattle_weather_selected" } ``` diff --git a/docs/assets/examples/seattle-weather/7.idoc.md b/docs/assets/examples/seattle-weather/7.idoc.md index 79e550ac..c0b0cdee 100644 --- a/docs/assets/examples/seattle-weather/7.idoc.md +++ b/docs/assets/examples/seattle-weather/7.idoc.md @@ -73,8 +73,8 @@ Data table: ```json tabulator { - "dataSignalName": "seattle_weather", - "options": {} + "dataSourceName": "seattle_weather", + "variableId": "seattle_weather_selected" } ``` diff --git a/docs/assets/examples/seattle-weather/8.idoc.md b/docs/assets/examples/seattle-weather/8.idoc.md index f4291845..d1609e88 100644 --- a/docs/assets/examples/seattle-weather/8.idoc.md +++ b/docs/assets/examples/seattle-weather/8.idoc.md @@ -99,8 +99,8 @@ Data table: ```json tabulator { - "dataSignalName": "seattle_weather", - "options": {} + "dataSourceName": "seattle_weather", + "variableId": "seattle_weather_selected" } ``` diff --git a/docs/assets/examples/seattle-weather/9.idoc.md b/docs/assets/examples/seattle-weather/9.idoc.md index cb2110bc..45f1d86b 100644 --- a/docs/assets/examples/seattle-weather/9.idoc.md +++ b/docs/assets/examples/seattle-weather/9.idoc.md @@ -166,8 +166,8 @@ Data table: ```json tabulator { - "dataSignalName": "seattle_weather", - "options": {} + "dataSourceName": "seattle_weather", + "variableId": "seattle_weather_selected" } ``` From 8a83cb806c900ea6a453d1150530a60448840e3d Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 18:08:01 -0700 Subject: [PATCH 56/61] new build --- docs/dist/idocs.compiler.umd.js | 269 +++-- docs/dist/idocs.editor.umd.js | 1724 +++++++++++++++++------------- docs/dist/idocs.host.umd.js | 1737 ++++++++++++++++++------------- docs/dist/idocs.sandbox.umd.js | 1383 ++++++++++++++---------- 4 files changed, 2932 insertions(+), 2181 deletions(-) diff --git a/docs/dist/idocs.compiler.umd.js b/docs/dist/idocs.compiler.umd.js index 4edfccc5..8f0fb135 100644 --- a/docs/dist/idocs.compiler.umd.js +++ b/docs/dist/idocs.compiler.umd.js @@ -6,7 +6,6 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const defaultCommonOptions = { - dataNameSelectedSuffix: "_selected", dataSignalPrefix: "data_signal:", groupClassName: "group" }; @@ -18,11 +17,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return name.replace(/[^a-zA-Z0-9_]/g, "_"); } function getChartType(spec) { - const $schema2 = spec == null ? void 0 : spec.$schema; - if (!$schema2) { + const $schema = spec == null ? void 0 : spec.$schema; + if (!$schema) { return "vega-lite"; } - return $schema2.includes("vega-lite") ? "vega-lite" : "vega"; + return $schema.includes("vega-lite") ? "vega-lite" : "vega"; } function changePageOrigin(page, oldOrigin, newOriginUrl) { const newPage = { @@ -58,21 +57,103 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }; return newPage; } - function addStaticDataLoaderToSpec(vegaScope, dataSource) { - const { spec } = vegaScope; - const { dataSourceName } = dataSource; - if (!spec.signals) { - spec.signals = []; + function topologicalSort(list) { + var _a; + const nameToObject = /* @__PURE__ */ new Map(); + const inDegree = /* @__PURE__ */ new Map(); + const graph = /* @__PURE__ */ new Map(); + for (const obj of list) { + nameToObject.set(obj.variableId, obj); + inDegree.set(obj.variableId, 0); + graph.set(obj.variableId, []); + } + for (const obj of list) { + const sources = ((_a = obj.calculation) == null ? void 0 : _a.dependsOn) || []; + for (const dep of sources) { + if (!graph.has(dep)) { + continue; + } + graph.get(dep).push(obj.variableId); + inDegree.set(obj.variableId, inDegree.get(obj.variableId) + 1); + } + } + const queue = []; + for (const [name, degree] of inDegree.entries()) { + if (degree === 0) queue.push(name); + } + const sorted = []; + while (queue.length) { + const current = queue.shift(); + sorted.push(nameToObject.get(current)); + for (const neighbor of graph.get(current)) { + inDegree.set(neighbor, inDegree.get(neighbor) - 1); + if (inDegree.get(neighbor) === 0) { + queue.push(neighbor); + } + } + } + if (sorted.length !== list.length) { + throw new Error("Cycle or missing dependency detected"); } - spec.signals.push( - { - name: dataSourceName, - update: `data('${dataSourceName}')` + return sorted; + } + function createSpecWithVariables(variables, tableElements, stubDataLoaders) { + const spec = { + $schema: "https://vega.github.io/schema/vega/v5.json", + signals: [], + data: [] + }; + tableElements.forEach((table) => { + const { variableId } = table; + spec.signals.push(dataAsSignal(variableId)); + spec.data.unshift({ + name: variableId, + values: [] + }); + }); + topologicalSort(variables).forEach((v) => { + if (isDataframePipeline(v)) { + const { dataFrameTransformations } = v.calculation; + const data = { + name: v.variableId, + source: v.calculation.dependsOn || [], + transform: dataFrameTransformations + }; + spec.data.push(data); + spec.signals.push(dataAsSignal(v.variableId)); + } else { + const signal = { name: v.variableId, value: v.initialValue }; + if (v.calculation) { + signal.update = v.calculation.vegaExpression; + } + spec.signals.push(signal); } - ); + }); + return spec; + } + function isDataframePipeline(variable) { + var _a, _b; + return variable.type === "object" && !!variable.isArray && (((_a = variable.calculation) == null ? void 0 : _a.dependsOn) !== void 0 && variable.calculation.dependsOn.length > 0 || ((_b = variable.calculation) == null ? void 0 : _b.dataFrameTransformations) !== void 0 && variable.calculation.dataFrameTransformations.length > 0); + } + function ensureDataAndSignalsArray(spec) { if (!spec.data) { spec.data = []; } + if (!spec.signals) { + spec.signals = []; + } + } + function dataAsSignal(name) { + return { + name, + update: `data('${name}')` + }; + } + function addStaticDataLoaderToSpec(vegaScope, dataSource) { + const { spec } = vegaScope; + const { dataSourceName } = dataSource; + ensureDataAndSignalsArray(spec); + spec.signals.push(dataAsSignal(dataSourceName)); const newData = { name: dataSourceName, values: [], @@ -93,18 +174,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const { dataSourceName } = dataSource; const urlSignal = vegaScope.createUrlSignal(dataSource.urlRef); const url = { signal: urlSignal.name }; - if (!spec.signals) { - spec.signals = []; - } - spec.signals.push( - { - name: dataSourceName, - update: `data('${dataSourceName}')` - } - ); - if (!spec.data) { - spec.data = []; - } + ensureDataAndSignalsArray(spec); + spec.signals.push(dataAsSignal(dataSourceName)); spec.data.unshift({ name: dataSourceName, url, @@ -156,82 +227,6 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return JSON.stringify(param.value); } } - function topologicalSort(list) { - var _a; - const nameToObject = /* @__PURE__ */ new Map(); - const inDegree = /* @__PURE__ */ new Map(); - const graph = /* @__PURE__ */ new Map(); - for (const obj of list) { - nameToObject.set(obj.variableId, obj); - inDegree.set(obj.variableId, 0); - graph.set(obj.variableId, []); - } - for (const obj of list) { - const sources = ((_a = obj.calculation) == null ? void 0 : _a.dependsOn) || []; - for (const dep of sources) { - if (!graph.has(dep)) { - continue; - } - graph.get(dep).push(obj.variableId); - inDegree.set(obj.variableId, inDegree.get(obj.variableId) + 1); - } - } - const queue = []; - for (const [name, degree] of inDegree.entries()) { - if (degree === 0) queue.push(name); - } - const sorted = []; - while (queue.length) { - const current = queue.shift(); - sorted.push(nameToObject.get(current)); - for (const neighbor of graph.get(current)) { - inDegree.set(neighbor, inDegree.get(neighbor) - 1); - if (inDegree.get(neighbor) === 0) { - queue.push(neighbor); - } - } - } - if (sorted.length !== list.length) { - throw new Error("Cycle or missing dependency detected"); - } - return sorted; - } - function createSpecWithVariables(dataNameSelectedSuffix, variables, stubDataLoaders) { - const spec = { - $schema: "https://vega.github.io/schema/vega/v5.json", - signals: [], - data: [] - }; - topologicalSort(variables).forEach((v) => { - if (isDataframePipeline(v)) { - const { dataFrameTransformations } = v.calculation; - const data = { - name: v.variableId, - source: v.calculation.dependsOn || [], - transform: dataFrameTransformations - }; - spec.data.push(data); - if (!spec.signals) { - spec.signals = []; - } - spec.signals.push({ - name: v.variableId, - update: `data('${v.variableId}')` - }); - } else { - const signal = { name: v.variableId, value: v.initialValue }; - if (v.calculation) { - signal.update = v.calculation.vegaExpression; - } - spec.signals.push(signal); - } - }); - return spec; - } - function isDataframePipeline(variable) { - var _a, _b; - return variable.type === "object" && !!variable.isArray && (((_a = variable.calculation) == null ? void 0 : _a.dependsOn) !== void 0 && variable.calculation.dependsOn.length > 0 || ((_b = variable.calculation) == null ? void 0 : _b.dataFrameTransformations) !== void 0 && variable.calculation.dataFrameTransformations.length > 0); - } function tickWrap(tick, content) { return `\`\`\`${tick} ${content} @@ -249,7 +244,6 @@ ${content} ${content} :::`; } - const $schema = "https://vega.github.io/schema/vega/v5.json"; function targetMarkdown(page) { var _a; const mdSections = []; @@ -258,7 +252,8 @@ ${content} if ((_a = page.layout) == null ? void 0 : _a.css) { mdSections.push(tickWrap("css", page.layout.css)); } - const vegaScope = dataLoaderMarkdown(dataLoaders.filter((dl) => dl.type !== "spec"), variables); + const tableElements = page.groups.flatMap((group) => group.elements.filter((e) => typeof e !== "string" && e.type === "table")); + const vegaScope = dataLoaderMarkdown(dataLoaders.filter((dl) => dl.type !== "spec"), variables, tableElements); for (const dataLoader of dataLoaders.filter((dl) => dl.type === "spec")) { mdSections.push(chartWrap(dataLoader.spec)); } @@ -269,8 +264,8 @@ ${content} const markdown = mdSections.join("\n\n"); return markdown; } - function dataLoaderMarkdown(dataSources, variables) { - const spec = createSpecWithVariables(defaultCommonOptions.dataNameSelectedSuffix, variables); + function dataLoaderMarkdown(dataSources, variables, tableElements) { + const spec = createSpecWithVariables(variables, tableElements); const vegaScope = new VegaScope(spec); for (const dataSource of dataSources) { switch (dataSource.type) { @@ -287,12 +282,6 @@ ${content} break; } } - if (!spec.data) { - spec.data = []; - } - spec.data.unshift({ - name: dataSource.dataSourceName + defaultCommonOptions.dataNameSelectedSuffix - }); } return vegaScope; } @@ -315,7 +304,7 @@ ${content} } case "checkbox": { const cbSpec = { - name: element.variableId, + variableId: element.variableId, value: (_a = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _a.initialValue, label: element.label }; @@ -324,13 +313,13 @@ ${content} } case "dropdown": { const ddSpec = { - name: element.variableId, + variableId: element.variableId, value: (_b = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _b.initialValue, label: element.label }; if (element.dynamicOptions) { ddSpec.dynamicOptions = { - dataSignalName: element.dynamicOptions.dataSourceName, + dataSourceName: element.dynamicOptions.dataSourceName, fieldName: element.dynamicOptions.fieldName }; } else { @@ -360,48 +349,32 @@ ${content} break; } case "slider": { - const spec = { - $schema, - signals: [ - { - name: element.variableId, - value: (_c = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _c.initialValue, - bind: { - input: "range", - min: element.min, - max: element.max, - step: element.step, - debounce: 100 - } - } - ] + const sliderSpec = { + variableId: element.variableId, + value: (_c = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _c.initialValue, + label: element.label, + min: element.min, + max: element.max, + step: element.step }; - mdElements.push(chartWrap(spec)); + mdElements.push(jsonWrap("slider", JSON.stringify(sliderSpec, null, 2))); break; } case "table": { - const tableSpec = { - dataSignalName: element.dataSourceName, - options: element.options - }; + const { dataSourceName, variableId, tabulatorOptions } = element; + const tableSpec = { dataSourceName, variableId, tabulatorOptions }; mdElements.push(jsonWrap("tabulator", JSON.stringify(tableSpec, null, 2))); break; } case "textbox": { - const spec = { - $schema, - signals: [ - { - name: element.variableId, - value: (_d = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _d.initialValue, - bind: { - input: "text", - debounce: 100 - } - } - ] + const textboxElement = element; + const textboxSpec = { + variableId: textboxElement.variableId, + value: (_d = variables.find((v) => v.variableId === textboxElement.variableId)) == null ? void 0 : _d.initialValue, + label: textboxElement.label, + multiline: textboxElement.multiline }; - mdElements.push(chartWrap(spec)); + mdElements.push(jsonWrap("textbox", JSON.stringify(textboxSpec, null, 2))); break; } } diff --git a/docs/dist/idocs.editor.umd.js b/docs/dist/idocs.editor.umd.js index 5fa23b4a..1ac500b3 100644 --- a/docs/dist/idocs.editor.umd.js +++ b/docs/dist/idocs.editor.umd.js @@ -6,7 +6,6 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const defaultCommonOptions = { - dataNameSelectedSuffix: "_selected", dataSignalPrefix: "data_signal:", groupClassName: "group" }; @@ -18,11 +17,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return name.replace(/[^a-zA-Z0-9_]/g, "_"); } function getChartType(spec) { - const $schema2 = spec == null ? void 0 : spec.$schema; - if (!$schema2) { + const $schema = spec == null ? void 0 : spec.$schema; + if (!$schema) { return "vega-lite"; } - return $schema2.includes("vega-lite") ? "vega-lite" : "vega"; + return $schema.includes("vega-lite") ? "vega-lite" : "vega"; } function changePageOrigin(page, oldOrigin, newOriginUrl) { const newPage = { @@ -58,19 +57,104 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }; return newPage; } - function addStaticDataLoaderToSpec(vegaScope, dataSource) { - const { spec } = vegaScope; - const { dataSourceName } = dataSource; - if (!spec.signals) { - spec.signals = []; + function topologicalSort(list) { + var _a; + const nameToObject = /* @__PURE__ */ new Map(); + const inDegree = /* @__PURE__ */ new Map(); + const graph = /* @__PURE__ */ new Map(); + for (const obj of list) { + nameToObject.set(obj.variableId, obj); + inDegree.set(obj.variableId, 0); + graph.set(obj.variableId, []); } - spec.signals.push({ - name: dataSourceName, - update: `data('${dataSourceName}')` + for (const obj of list) { + const sources = ((_a = obj.calculation) == null ? void 0 : _a.dependsOn) || []; + for (const dep of sources) { + if (!graph.has(dep)) { + continue; + } + graph.get(dep).push(obj.variableId); + inDegree.set(obj.variableId, inDegree.get(obj.variableId) + 1); + } + } + const queue = []; + for (const [name, degree] of inDegree.entries()) { + if (degree === 0) + queue.push(name); + } + const sorted = []; + while (queue.length) { + const current = queue.shift(); + sorted.push(nameToObject.get(current)); + for (const neighbor of graph.get(current)) { + inDegree.set(neighbor, inDegree.get(neighbor) - 1); + if (inDegree.get(neighbor) === 0) { + queue.push(neighbor); + } + } + } + if (sorted.length !== list.length) { + throw new Error("Cycle or missing dependency detected"); + } + return sorted; + } + function createSpecWithVariables(variables, tableElements, stubDataLoaders) { + const spec = { + $schema: "https://vega.github.io/schema/vega/v5.json", + signals: [], + data: [] + }; + tableElements.forEach((table) => { + const { variableId } = table; + spec.signals.push(dataAsSignal(variableId)); + spec.data.unshift({ + name: variableId, + values: [] + }); + }); + topologicalSort(variables).forEach((v) => { + if (isDataframePipeline(v)) { + const { dataFrameTransformations } = v.calculation; + const data = { + name: v.variableId, + source: v.calculation.dependsOn || [], + transform: dataFrameTransformations + }; + spec.data.push(data); + spec.signals.push(dataAsSignal(v.variableId)); + } else { + const signal = { name: v.variableId, value: v.initialValue }; + if (v.calculation) { + signal.update = v.calculation.vegaExpression; + } + spec.signals.push(signal); + } }); + return spec; + } + function isDataframePipeline(variable) { + var _a, _b; + return variable.type === "object" && !!variable.isArray && (((_a = variable.calculation) == null ? void 0 : _a.dependsOn) !== void 0 && variable.calculation.dependsOn.length > 0 || ((_b = variable.calculation) == null ? void 0 : _b.dataFrameTransformations) !== void 0 && variable.calculation.dataFrameTransformations.length > 0); + } + function ensureDataAndSignalsArray(spec) { if (!spec.data) { spec.data = []; } + if (!spec.signals) { + spec.signals = []; + } + } + function dataAsSignal(name) { + return { + name, + update: `data('${name}')` + }; + } + function addStaticDataLoaderToSpec(vegaScope, dataSource) { + const { spec } = vegaScope; + const { dataSourceName } = dataSource; + ensureDataAndSignalsArray(spec); + spec.signals.push(dataAsSignal(dataSourceName)); const newData = { name: dataSourceName, values: [], @@ -91,16 +175,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const { dataSourceName } = dataSource; const urlSignal = vegaScope.createUrlSignal(dataSource.urlRef); const url = { signal: urlSignal.name }; - if (!spec.signals) { - spec.signals = []; - } - spec.signals.push({ - name: dataSourceName, - update: `data('${dataSourceName}')` - }); - if (!spec.data) { - spec.data = []; - } + ensureDataAndSignalsArray(spec); + spec.signals.push(dataAsSignal(dataSourceName)); spec.data.unshift({ name: dataSourceName, url, @@ -153,83 +229,6 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return JSON.stringify(param.value); } } - function topologicalSort(list) { - var _a; - const nameToObject = /* @__PURE__ */ new Map(); - const inDegree = /* @__PURE__ */ new Map(); - const graph = /* @__PURE__ */ new Map(); - for (const obj of list) { - nameToObject.set(obj.variableId, obj); - inDegree.set(obj.variableId, 0); - graph.set(obj.variableId, []); - } - for (const obj of list) { - const sources = ((_a = obj.calculation) == null ? void 0 : _a.dependsOn) || []; - for (const dep of sources) { - if (!graph.has(dep)) { - continue; - } - graph.get(dep).push(obj.variableId); - inDegree.set(obj.variableId, inDegree.get(obj.variableId) + 1); - } - } - const queue = []; - for (const [name, degree] of inDegree.entries()) { - if (degree === 0) - queue.push(name); - } - const sorted = []; - while (queue.length) { - const current = queue.shift(); - sorted.push(nameToObject.get(current)); - for (const neighbor of graph.get(current)) { - inDegree.set(neighbor, inDegree.get(neighbor) - 1); - if (inDegree.get(neighbor) === 0) { - queue.push(neighbor); - } - } - } - if (sorted.length !== list.length) { - throw new Error("Cycle or missing dependency detected"); - } - return sorted; - } - function createSpecWithVariables(dataNameSelectedSuffix, variables, stubDataLoaders) { - const spec = { - $schema: "https://vega.github.io/schema/vega/v5.json", - signals: [], - data: [] - }; - topologicalSort(variables).forEach((v) => { - if (isDataframePipeline(v)) { - const { dataFrameTransformations } = v.calculation; - const data = { - name: v.variableId, - source: v.calculation.dependsOn || [], - transform: dataFrameTransformations - }; - spec.data.push(data); - if (!spec.signals) { - spec.signals = []; - } - spec.signals.push({ - name: v.variableId, - update: `data('${v.variableId}')` - }); - } else { - const signal = { name: v.variableId, value: v.initialValue }; - if (v.calculation) { - signal.update = v.calculation.vegaExpression; - } - spec.signals.push(signal); - } - }); - return spec; - } - function isDataframePipeline(variable) { - var _a, _b; - return variable.type === "object" && !!variable.isArray && (((_a = variable.calculation) == null ? void 0 : _a.dependsOn) !== void 0 && variable.calculation.dependsOn.length > 0 || ((_b = variable.calculation) == null ? void 0 : _b.dataFrameTransformations) !== void 0 && variable.calculation.dataFrameTransformations.length > 0); - } function tickWrap(tick, content) { return `\`\`\`${tick} ${content} @@ -247,7 +246,6 @@ ${content} ${content} :::`; } - const $schema = "https://vega.github.io/schema/vega/v5.json"; function targetMarkdown(page) { var _a; const mdSections = []; @@ -256,7 +254,8 @@ ${content} if ((_a = page.layout) == null ? void 0 : _a.css) { mdSections.push(tickWrap("css", page.layout.css)); } - const vegaScope = dataLoaderMarkdown(dataLoaders.filter((dl) => dl.type !== "spec"), variables); + const tableElements = page.groups.flatMap((group) => group.elements.filter((e) => typeof e !== "string" && e.type === "table")); + const vegaScope = dataLoaderMarkdown(dataLoaders.filter((dl) => dl.type !== "spec"), variables, tableElements); for (const dataLoader of dataLoaders.filter((dl) => dl.type === "spec")) { mdSections.push(chartWrap(dataLoader.spec)); } @@ -267,8 +266,8 @@ ${content} const markdown = mdSections.join("\n\n"); return markdown; } - function dataLoaderMarkdown(dataSources, variables) { - const spec = createSpecWithVariables(defaultCommonOptions.dataNameSelectedSuffix, variables); + function dataLoaderMarkdown(dataSources, variables, tableElements) { + const spec = createSpecWithVariables(variables, tableElements); const vegaScope = new VegaScope(spec); for (const dataSource of dataSources) { switch (dataSource.type) { @@ -285,12 +284,6 @@ ${content} break; } } - if (!spec.data) { - spec.data = []; - } - spec.data.unshift({ - name: dataSource.dataSourceName + defaultCommonOptions.dataNameSelectedSuffix - }); } return vegaScope; } @@ -313,7 +306,7 @@ ${content} } case "checkbox": { const cbSpec = { - name: element.variableId, + variableId: element.variableId, value: (_a = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _a.initialValue, label: element.label }; @@ -322,13 +315,13 @@ ${content} } case "dropdown": { const ddSpec = { - name: element.variableId, + variableId: element.variableId, value: (_b = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _b.initialValue, label: element.label }; if (element.dynamicOptions) { ddSpec.dynamicOptions = { - dataSignalName: element.dynamicOptions.dataSourceName, + dataSourceName: element.dynamicOptions.dataSourceName, fieldName: element.dynamicOptions.fieldName }; } else { @@ -358,48 +351,32 @@ ${content} break; } case "slider": { - const spec = { - $schema, - signals: [ - { - name: element.variableId, - value: (_c = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _c.initialValue, - bind: { - input: "range", - min: element.min, - max: element.max, - step: element.step, - debounce: 100 - } - } - ] + const sliderSpec = { + variableId: element.variableId, + value: (_c = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _c.initialValue, + label: element.label, + min: element.min, + max: element.max, + step: element.step }; - mdElements.push(chartWrap(spec)); + mdElements.push(jsonWrap("slider", JSON.stringify(sliderSpec, null, 2))); break; } case "table": { - const tableSpec = { - dataSignalName: element.dataSourceName, - options: element.options - }; + const { dataSourceName, variableId, tabulatorOptions } = element; + const tableSpec = { dataSourceName, variableId, tabulatorOptions }; mdElements.push(jsonWrap("tabulator", JSON.stringify(tableSpec, null, 2))); break; } case "textbox": { - const spec = { - $schema, - signals: [ - { - name: element.variableId, - value: (_d = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _d.initialValue, - bind: { - input: "text", - debounce: 100 - } - } - ] + const textboxElement = element; + const textboxSpec = { + variableId: textboxElement.variableId, + value: (_d = variables.find((v) => v.variableId === textboxElement.variableId)) == null ? void 0 : _d.initialValue, + label: textboxElement.label, + multiline: textboxElement.multiline }; - mdElements.push(chartWrap(spec)); + mdElements.push(jsonWrap("textbox", JSON.stringify(textboxSpec, null, 2))); break; } } @@ -462,7 +439,6 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const defaultCommonOptions = { - dataNameSelectedSuffix: "_selected", dataSignalPrefix: "data_signal:", groupClassName: "group" }; @@ -771,7 +747,21 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy */ const plugins = []; function registerMarkdownPlugin(plugin) { - plugins.push(plugin); + let insertIndex = plugins.length; + let minIndex = 0; + for (let i = 0; i < plugins.length; i++) { + if (plugins[i].hydratesBefore === plugin.name) { + minIndex = Math.max(minIndex, i + 1); + } + } + if (plugin.hydratesBefore) { + const targetIndex = plugins.findIndex((p) => p.name === plugin.hydratesBefore); + if (targetIndex !== -1) { + insertIndex = targetIndex; + } + } + insertIndex = Math.max(insertIndex, minIndex); + plugins.splice(insertIndex, 0, plugin); return "register"; } function create() { @@ -787,8 +777,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const token = tokens[idx]; const info = token.info.trim(); if (info.startsWith("json ")) { - const pluginName = info.slice(5).trim(); - const plugin = plugins.find((p) => p.name === pluginName); + const pluginName2 = info.slice(5).trim(); + const plugin = plugins.find((p) => p.name === pluginName2); if (plugin && plugin.fence) { return plugin.fence(token, idx); } @@ -801,11 +791,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }; return md; } - function definePlugin(md, pluginName) { - md.block.ruler.before("fence", \`\${pluginName}_block\`, function(state, startLine, endLine) { + function definePlugin(md, pluginName2) { + md.block.ruler.before("fence", \`\${pluginName2}_block\`, function(state, startLine, endLine) { const start = state.bMarks[startLine] + state.tShift[startLine]; const max = state.eMarks[startLine]; - const marker = \`json \${pluginName}\`; + const marker = \`json \${pluginName2}\`; if (!state.src.slice(start, max).trim().startsWith("\`\`\`" + marker)) { return false; } @@ -824,291 +814,138 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return true; }); } + function urlParam(urlParamName, value) { + if (value === void 0 || value === null) return ""; + if (Array.isArray(value)) { + return value.map((vn) => \`\${urlParamName}[]=\${encodeURIComponent(vn)}\`).join("&"); + } else { + return \`\${urlParamName}=\${encodeURIComponent(value)}\`; + } + } + function getJsonScriptTag(container, errorHandler) { + const scriptTag = container.previousElementSibling; + if ((scriptTag == null ? void 0 : 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 { + return JSON.parse(scriptTag.textContent); + } catch (error) { + errorHandler(error); + return null; + } + } + function pluginClassName(pluginName2) { + return \`chartifact-plugin-\${pluginName2}\`; + } /*! * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ - var LogLevel = /* @__PURE__ */ ((LogLevel2) => { - LogLevel2[LogLevel2["none"] = 0] = "none"; - LogLevel2[LogLevel2["some"] = 1] = "some"; - LogLevel2[LogLevel2["all"] = 2] = "all"; - return LogLevel2; - })(LogLevel || {}); - class SignalBus { - constructor(dataSignalPrefix) { - __publicField(this, "broadcastingStack"); - __publicField(this, "logLevel"); - __publicField(this, "logWatchIds"); - __publicField(this, "active"); - __publicField(this, "peers"); - __publicField(this, "signalDeps"); - __publicField(this, "peerDependencies"); - this.dataSignalPrefix = dataSignalPrefix; - this.logLevel = 0; - this.logWatchIds = []; - this.reset(); - } - log(id, message, ...optionalParams) { - if (this.logLevel === 0) return; - if (this.logWatchIds.length > 0 && !this.logWatchIds.includes(id)) return; - console.log(\`[Signal Bus][\${id}] \${message}\`, ...optionalParams); + function sanitizedHTML(tagName, attributes, content, precedeWithScriptTag) { + const element = document.createElement(tagName); + Object.keys(attributes).forEach((key) => { + element.setAttribute(key, attributes[key]); + }); + if (precedeWithScriptTag) { + const scriptElement = document.createElement("script"); + scriptElement.setAttribute("type", "application/json"); + const safeContent = content.replace(/<\\/script>/gi, "<\\\\/script>"); + scriptElement.innerHTML = safeContent; + return scriptElement.outerHTML + element.outerHTML; + } else { + element.textContent = content; } - async broadcast(originId, batch) { - if (this.broadcastingStack.includes(originId)) { - this.log(originId, "Additional broadcast from", originId, this.broadcastingStack.join(" -> ")); - } - this.log(originId, "Broadcasting batch from", originId, batch); - this.broadcastingStack.push(originId); - for (const peerId of this.peerDependencies[originId]) { - const peer = this.peers.find((p) => p.id === peerId); - if (!peer) continue; - const peerBatch = {}; - let hasBatch = false; - for (const signalName in batch) { - if (peer.initialSignals.some((s) => s.name === signalName) && batch[signalName].value !== this.signalDeps[signalName].value) { - peerBatch[signalName] = batch[signalName]; - hasBatch = true; + return element.outerHTML; + } + function flaggableJsonPlugin(pluginName2, className2, flagger, attrs) { + const plugin = { + name: pluginName2, + initializePlugin: (md) => definePlugin(md, pluginName2), + fence: (token, index2) => { + let json = token.content.trim(); + let spec; + let flaggableSpec; + try { + spec = JSON.parse(json); + } catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reasons: [\`malformed JSON\`] + }; + } + if (spec) { + if (flagger) { + flaggableSpec = flagger(spec); + } else { + flaggableSpec = { spec }; } } - if (!hasBatch) continue; - peer.recieveBatch && await peer.recieveBatch(peerBatch, originId); + if (flaggableSpec) { + json = JSON.stringify(flaggableSpec); + } + return sanitizedHTML("div", { class: className2, id: \`\${pluginName2}-\${index2}\`, ...attrs }, json, true); + }, + hydrateSpecs: (renderer, errorHandler) => { + var _a; + const flagged = []; + const containers = renderer.element.querySelectorAll(\`.\${className2}\`); + for (const [index2, container] of Array.from(containers).entries()) { + const flaggableSpec = getJsonScriptTag(container, (e) => errorHandler(e, pluginName2, index2, "parse", container)); + if (!flaggableSpec) continue; + const f = { approvedSpec: null, pluginName: pluginName2, containerId: container.id }; + if (flaggableSpec.hasFlags) { + f.blockedSpec = flaggableSpec.spec; + f.reason = ((_a = flaggableSpec.reasons) == null ? void 0 : _a.join(", ")) || "Unknown reason"; + } else { + f.approvedSpec = flaggableSpec.spec; + } + flagged.push(f); + } + return flagged; } - this.broadcastingStack.pop(); - for (const signalName in batch) { - const signalDep = this.signalDeps[signalName]; - signalDep.value = batch[signalName].value; - } - if (this.broadcastingStack.length === 0) { - for (const peer of this.peers) { - peer.broadcastComplete && await peer.broadcastComplete(); - } - } - } - getPriorityPeer(signalName) { - const signalDep = this.signalDeps[signalName]; - if (!signalDep) return null; - return this.peers.find((p) => p.id === signalDep.initialPriorityId); - } - registerPeer(peer) { - this.peers.push(peer); - for (const initialSignal of peer.initialSignals) { - if (!(initialSignal.name in this.signalDeps)) { - this.signalDeps[initialSignal.name] = { - deps: [peer], - priority: initialSignal.priority, - initialPriorityId: peer.id, - value: initialSignal.value, - isData: initialSignal.isData - }; - } else { - const signalDep = this.signalDeps[initialSignal.name]; - if (!signalDep.deps.includes(peer)) { - signalDep.deps.push(peer); - } - if (initialSignal.priority > signalDep.priority) { - signalDep.priority = initialSignal.priority; - signalDep.initialPriorityId = peer.id; - signalDep.value = initialSignal.value; - signalDep.isData = initialSignal.isData; - } - } - } - } - beginListening() { - this.log("beginListening", "begin initial batch", this.signalDeps); - for (const peer of this.peers) { - const batch = {}; - for (const signalName in this.signalDeps) { - const signalDep = this.signalDeps[signalName]; - const { value, isData } = signalDep; - batch[signalName] = { value, isData }; - } - peer.recieveBatch && peer.recieveBatch(batch, "initial"); - } - this.log("beginListening", "end initial batch"); - const peerSignals = {}; - for (const signalName in this.signalDeps) { - const signalDep = this.signalDeps[signalName]; - if (signalDep.deps.length === 1) continue; - for (const peer of signalDep.deps) { - if (!(peer.id in peerSignals)) { - peerSignals[peer.id] = []; - this.peerDependencies[peer.id] = []; - } - peerSignals[peer.id].push({ signalName, isData: signalDep.isData }); - for (const otherPeer of signalDep.deps) { - if (otherPeer.id !== peer.id && !this.peerDependencies[peer.id].includes(otherPeer.id)) { - this.peerDependencies[peer.id].push(otherPeer.id); - } - } - } - } - this.log("beginListening", "======= dependencies =========", peerSignals, this.peerDependencies); - for (const peer of this.peers) { - const sharedSignals = peerSignals[peer.id]; - if (sharedSignals) { - this.log(peer.id, "Shared signals:", sharedSignals); - if (this.peerDependencies[peer.id]) { - this.log(peer.id, "Shared dependencies:", this.peerDependencies[peer.id]); - } - peer.beginListening && peer.beginListening(sharedSignals); - } else { - this.log(peer.id, "No shared signals"); - } - } - this.active = true; - } - reset() { - this.signalDeps = {}; - this.active = false; - this.peers = []; - this.broadcastingStack = []; - this.peerDependencies = {}; - } - } - /*! - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ - const defaultRendererOptions = { - vegaRenderer: "canvas", - useShadowDom: false, - errorHandler: (error, pluginName, instanceIndex, phase) => { - console.error(\`Error in plugin \${pluginName} instance \${instanceIndex} phase \${phase}\`, error); - } - }; - class Renderer { - constructor(_element, options) { - __publicField(this, "md"); - __publicField(this, "instances"); - __publicField(this, "signalBus"); - __publicField(this, "options"); - __publicField(this, "shadowRoot"); - __publicField(this, "element"); - this.options = { ...defaultRendererOptions, ...options }; - this.signalBus = this.options.signalBus || new SignalBus(defaultCommonOptions.dataSignalPrefix); - this.instances = {}; - if (this.options.useShadowDom) { - this.shadowRoot = _element.attachShadow({ mode: "open" }); - this.element = this.shadowRoot; - } else { - this.element = _element; - } - } - ensureMd() { - if (!this.md) { - this.md = create(); - } - } - async render(markdown) { - await this.reset(); - const content = this.renderHtml(markdown); - this.element.innerHTML = content; - await this.hydrate(); - } - renderHtml(markdown) { - this.ensureMd(); - const parsedHTML = this.md.render(markdown); - let content = parsedHTML; - if (this.options.useShadowDom) { - content = \`
\${content}
\`; - } - return content; - } - async hydrate() { - this.ensureMd(); - this.signalBus.log("Renderer", "rendering DOM"); - const hydrationPromises = []; - for (let i = 0; i < plugins.length; i++) { - const plugin = plugins[i]; - if (plugin.hydrateComponent) { - hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler).then((instances) => { - return { - pluginName: plugin.name, - instances - }; - })); - } - } - try { - const pluginHydrations = await Promise.all(hydrationPromises); - for (const hydration of pluginHydrations) { - if (hydration && hydration.instances) { - this.instances[hydration.pluginName] = hydration.instances; - for (const instance of hydration.instances) { - this.signalBus.registerPeer(instance); - } - } - } - this.signalBus.beginListening(); - } catch (error) { - console.error("Error in rendering plugins", error); - } - } - reset() { - this.signalBus.reset(); - for (const pluginName of Object.keys(this.instances)) { - const instances = this.instances[pluginName]; - for (const instance of instances) { - instance.destroy && instance.destroy(); - } - } - this.instances = {}; - this.element.innerHTML = ""; - } - } - /*! - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ - function sanitizedHTML(tagName, attributes, content) { - const element = document.createElement(tagName); - Object.keys(attributes).forEach((key) => { - element.setAttribute(key, attributes[key]); - }); - element.textContent = content; - return element.outerHTML; + }; + return plugin; } /*! * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$a = "checkbox"; + const className$9 = pluginClassName(pluginName$a); const checkboxPlugin = { - name: "checkbox", - initializePlugin: (md) => definePlugin(md, "checkbox"), - fence: (token, idx) => { - const CheckboxId = \`Checkbox-\${idx}\`; - return sanitizedHTML("div", { id: CheckboxId, class: "checkbox" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$a, className$9), + hydrateComponent: async (renderer, errorHandler, specs) => { const checkboxInstances = []; - const containers = renderer.element.querySelectorAll(".checkbox"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const spec = JSON.parse(container.textContent); - const html = \`
+ for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const html = \`
\`; - container.innerHTML = html; - const element = container.querySelector('input[type="checkbox"]'); - const checkboxInstance = { id: container.id, spec, element }; - checkboxInstances.push(checkboxInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "Checkbox", index2, "parse", container); - continue; - } + container.innerHTML = html; + const element = container.querySelector('input[type="checkbox"]'); + const checkboxInstance = { id: \`\${pluginName$a}-\${index2}\`, spec, element }; + checkboxInstances.push(checkboxInstance); } const instances = checkboxInstances.map((checkboxInstance) => { const { element, spec } = checkboxInstance; const initialSignals = [{ - name: spec.name, + name: spec.variableId, value: spec.value || false, priority: 1, isData: false @@ -1117,8 +954,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy ...checkboxInstance, initialSignals, recieveBatch: async (batch) => { - if (batch[spec.name]) { - const value = batch[spec.name].value; + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; element.checked = value; } }, @@ -1126,7 +963,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy element.addEventListener("change", (e) => { const value = e.target.checked; const batch = { - [spec.name]: { + [spec.variableId]: { value, isData: false } @@ -1183,9 +1020,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return cssBlocks.join("\\n\\n"); } function categorizeCss(cssContent) { + const spec = { + atRules: {} + }; const result = { - atRules: {}, - hasFlags: false + spec, + hasFlags: false, + reasons: [] }; const completeBlockAtRules = [ // Keyframes and variants @@ -1250,16 +1091,16 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy let addCurrentRule = function() { 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]; } } }; @@ -1271,26 +1112,28 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const atRuleSignature = \`@\${node.name}\${node.prelude ? \` \${csstree.generate(node.prelude)}\` : ""}\`; if (node.name === "import") { const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + 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; } if (completeBlockAtRules.includes(node.name)) { const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, css: ruleContent }; return; } if (node.block) { - if (!result.atRules[atRuleSignature]) { - result.atRules[atRuleSignature] = { + if (!spec.atRules[atRuleSignature]) { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, rules: [] }; @@ -1298,7 +1141,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy currentAtRuleSignature = atRuleSignature; } else { const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, css: ruleContent }; @@ -1320,6 +1163,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy declaration.flag = securityCheck.flag; declaration.reason = securityCheck.reason; result.hasFlags = true; + result.reasons.push(securityCheck.reason); } currentRule.declarations.push(declaration); } else if (currentRule && (node.type === "Function" || node.type === "Url" || node.type === "String" || node.type === "Identifier")) { @@ -1332,6 +1176,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy lastDecl.flag = securityCheck.flag; lastDecl.reason = securityCheck.reason; result.hasFlags = true; + result.reasons.push(securityCheck.reason); } } } @@ -1342,17 +1187,19 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } return result; } + const pluginName$9 = "css"; + const className$8 = pluginClassName(pluginName$9); const cssPlugin = { - name: "css", + ...flaggableJsonPlugin(pluginName$9, className$8), initializePlugin: (md) => { if (typeof csstree === "undefined") { throw new Error("css-tree library is required for CSS plugin. Please include the css-tree script."); } - definePlugin(md, "css"); + definePlugin(md, pluginName$9); md.block.ruler.before("fence", "css_block", function(state, startLine, endLine) { const start = state.bMarks[startLine] + state.tShift[startLine]; const max = state.eMarks[startLine]; - if (!state.src.slice(start, max).trim().startsWith("\`\`\`css")) { + if (!state.src.slice(start, max).trim().startsWith(\`\\\`\\\`\\\`\${pluginName$9}\`)) { return false; } let nextLine = startLine; @@ -1364,7 +1211,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } state.line = nextLine + 1; const token = state.push("fence", "code", 0); - token.info = "css"; + token.info = pluginName$9; token.content = state.getLines(startLine + 1, nextLine, state.blkIndent, true); token.map = [startLine, state.line]; return true; @@ -1373,11 +1220,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy md.renderer.rules.fence = function(tokens, idx, options, env, slf) { const token = tokens[idx]; const info = token.info.trim(); - if (info === "css") { - const cssId = \`css-\${idx}\`; + if (info === pluginName$9) { const cssContent = token.content.trim(); const categorizedCss = categorizeCss(cssContent); - return sanitizedHTML("div", { id: cssId, class: "css-component" }, JSON.stringify(categorizedCss)); + return sanitizedHTML("div", { id: \`\${pluginName$9}-\${idx}\`, class: className$8 }, JSON.stringify(categorizedCss), true); } if (originalFence) { return originalFence(tokens, idx, options, env, slf); @@ -1386,40 +1232,33 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } }; }, - hydrateComponent: async (renderer, errorHandler) => { + hydrateComponent: async (renderer, errorHandler, specs) => { const cssInstances = []; - const containers = renderer.element.querySelectorAll(".css-component"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const categorizedCss = JSON.parse(container.textContent); - const comments = []; - if (categorizedCss.hasFlags) { - console.warn(\`CSS security: Security issues detected in CSS\`); - comments.push(\`\`); - } - const safeCss = reconstituteCss(categorizedCss.atRules); - if (safeCss.trim().length > 0) { - const styleElement = document.createElement("style"); - styleElement.type = "text/css"; - styleElement.id = \`idocs-css-\${container.id}\`; - styleElement.textContent = safeCss; - const target = renderer.shadowRoot || document.head; - target.appendChild(styleElement); - comments.push(\`\`); - cssInstances.push({ - id: container.id, - element: styleElement - }); - } else { - comments.push(\`\`); - } - container.innerHTML = comments.join("\\n"); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "CSS", index2, "parse", container); + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { continue; } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const categorizedCss = specReview.approvedSpec; + const comments = []; + const safeCss = reconstituteCss(categorizedCss.atRules); + if (safeCss.trim().length > 0) { + const styleElement = document.createElement("style"); + styleElement.type = "text/css"; + styleElement.id = \`idocs-css-\${index2}\`; + styleElement.textContent = safeCss; + const target = renderer.shadowRoot || document.head; + target.appendChild(styleElement); + comments.push(\`\`); + cssInstances.push({ + id: \`\${pluginName$9}-\${index2}\`, + element: styleElement + }); + } else { + comments.push(\`\`); + } + container.innerHTML = comments.join("\\n"); } const instances = cssInstances.map((cssInstance) => { return { @@ -1440,51 +1279,45 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$8 = "dropdown"; + const className$7 = pluginClassName(pluginName$8); const dropdownPlugin = { - name: "dropdown", - initializePlugin: (md) => definePlugin(md, "dropdown"), - fence: (token, idx) => { - const DropdownId = \`Dropdown-\${idx}\`; - return sanitizedHTML("div", { id: DropdownId, class: "dropdown" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$8, className$7), + hydrateComponent: async (renderer, errorHandler, specs) => { const dropdownInstances = []; - const containers = renderer.element.querySelectorAll(".dropdown"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const spec = JSON.parse(container.textContent); - const html = \`
+ for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const html = \`
\`; - container.innerHTML = html; - const element = container.querySelector("select"); - setSelectOptions(element, spec.multiple ?? false, spec.options ?? [], spec.value ?? (spec.multiple ? [] : "")); - const dropdownInstance = { id: container.id, spec, element }; - dropdownInstances.push(dropdownInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "Dropdown", index2, "parse", container); - continue; - } + container.innerHTML = html; + const element = container.querySelector("select"); + setSelectOptions(element, spec.multiple ?? false, spec.options ?? [], spec.value ?? (spec.multiple ? [] : "")); + const dropdownInstance = { id: \`\${pluginName$8}-\${index2}\`, spec, element }; + dropdownInstances.push(dropdownInstance); } const instances = dropdownInstances.map((dropdownInstance, index2) => { const { element, spec } = dropdownInstance; const initialSignals = [{ - name: spec.name, + name: spec.variableId, value: spec.value || null, priority: 1, isData: false }]; if (spec.dynamicOptions) { initialSignals.push({ - name: spec.dynamicOptions.dataSignalName, + name: spec.dynamicOptions.dataSourceName, value: null, priority: -1, isData: true @@ -1496,8 +1329,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy recieveBatch: async (batch) => { var _a, _b; const { dynamicOptions } = spec; - if (dynamicOptions == null ? void 0 : dynamicOptions.dataSignalName) { - const newData = (_a = batch[dynamicOptions.dataSignalName]) == null ? void 0 : _a.value; + if (dynamicOptions == null ? void 0 : dynamicOptions.dataSourceName) { + const newData = (_a = batch[dynamicOptions.dataSourceName]) == null ? void 0 : _a.value; if (newData) { let hasFieldName = false; const uniqueOptions = /* @__PURE__ */ new Set(); @@ -1512,7 +1345,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const existingSelection = spec.multiple ? Array.from(element.selectedOptions).map((option) => option.value) : element.value; setSelectOptions(element, spec.multiple ?? false, options, existingSelection); if (!spec.multiple) { - element.value = ((_b = batch[spec.name]) == null ? void 0 : _b.value) || options[0]; + element.value = ((_b = batch[spec.variableId]) == null ? void 0 : _b.value) || options[0]; } } else { element.innerHTML = ""; @@ -1524,8 +1357,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } } } - if (batch[spec.name]) { - const value = batch[spec.name].value; + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; if (spec.multiple) { Array.from(element.options).forEach((option) => { option.selected = !!(value && Array.isArray(value) && value.includes(option.value)); @@ -1539,7 +1372,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy element.addEventListener("change", (e) => { const value = spec.multiple ? Array.from(e.target.selectedOptions).map((option) => option.value) : e.target.value; const batch = { - [spec.name]: { + [spec.variableId]: { value, isData: false } @@ -1599,51 +1432,46 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$7 = "image"; + const className$6 = pluginClassName(pluginName$7); const imagePlugin = { - name: "image", - initializePlugin: (md) => definePlugin(md, "image"), - fence: (token, idx) => { - const ImageId = \`Image-\${idx}\`; - return sanitizedHTML("div", { id: ImageId, class: "image" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$7, className$6), + hydrateComponent: async (renderer, errorHandler, specs) => { const imageInstances = []; - const containers = renderer.element.querySelectorAll(".image"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const spec = JSON.parse(container.textContent); - const element = document.createElement("img"); - const spinner = document.createElement("div"); - spinner.innerHTML = \` + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const element = document.createElement("img"); + const spinner = document.createElement("div"); + spinner.innerHTML = \` \`; - if (spec.alt) element.alt = spec.alt; - if (spec.width) element.width = spec.width; - if (spec.height) element.height = spec.height; - element.onload = () => { - spinner.style.display = "none"; - element.style.opacity = "1"; - }; - element.onerror = () => { - spinner.style.display = "none"; - element.style.opacity = "0.5"; - errorHandler(new Error("Image failed to load"), "image", index2, "load", container, element.src); - }; - container.style.position = "relative"; - spinner.style.position = "absolute"; - container.innerHTML = ""; - container.appendChild(spinner); - container.appendChild(element); - const imageInstance = { id: container.id, spec, element, spinner }; - imageInstances.push(imageInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "Image", index2, "parse", container); - } + if (spec.alt) element.alt = spec.alt; + if (spec.width) element.width = spec.width; + if (spec.height) element.height = spec.height; + element.onload = () => { + spinner.style.display = "none"; + element.style.opacity = "1"; + }; + element.onerror = () => { + spinner.style.display = "none"; + element.style.opacity = "0.5"; + errorHandler(new Error("Image failed to load"), pluginName$7, index2, "load", container, element.src); + }; + container.style.position = "relative"; + spinner.style.position = "absolute"; + container.innerHTML = ""; + container.appendChild(spinner); + container.appendChild(element); + const imageInstance = { id: \`\${pluginName$7}-\${index2}\`, spec, element, spinner }; + imageInstances.push(imageInstance); } const instances = imageInstances.map((imageInstance, index2) => { const { element, spinner, id, spec } = imageInstance; @@ -1713,8 +1541,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } return token; } + const pluginName$6 = "placeholders"; const placeholdersPlugin = { - name: "placeholders", + name: pluginName$6, initializePlugin: async (md) => { md.use(function(md2) { md2.inline.ruler.after("emphasis", "dynamic_placeholder", function(state, silent) { @@ -1802,7 +1631,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }); const instances = [ { - id: "placeholders", + id: pluginName$6, initialSignals, recieveBatch: async (batch) => { var _a; @@ -1849,28 +1678,20 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$5 = "presets"; + const className$5 = pluginClassName(pluginName$5); const presetsPlugin = { - name: "presets", - initializePlugin: (md) => definePlugin(md, "presets"), - fence: (token, idx) => { - const spec = JSON.parse(token.content.trim()); - const pluginId = \`preset-\${idx}\`; - return sanitizedHTML("div", { id: pluginId, class: "presets" }, JSON.stringify(spec)); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$5, className$5), + hydrateComponent: async (renderer, errorHandler, specs) => { const presetsInstances = []; - const containers = renderer.element.querySelectorAll(".presets"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - const id = \`presets\${index2}\`; - let presets; - try { - presets = JSON.parse(container.textContent); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "presets", index2, "parse", container); + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { continue; } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const id = \`\${pluginName$5}-\${index2}\`; + const presets = specReview.approvedSpec; if (!Array.isArray(presets)) { container.innerHTML = '
Expected an array of presets
'; continue; @@ -1955,75 +1776,164 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ - const tabulatorPlugin = { - name: "tabulator", - initializePlugin: (md) => definePlugin(md, "tabulator"), - fence: (token, idx) => { - const tabulatorId = \`tabulator-\${idx}\`; - return sanitizedHTML("div", { id: tabulatorId, class: "tabulator", style: "box-sizing: border-box;" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { - const tabulatorInstances = []; - const containers = renderer.element.querySelectorAll(".tabulator"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - if (!Tabulator) { - errorHandler(new Error("Tabulator not found"), "tabulator", index2, "init", container); - continue; - } - try { - const spec = JSON.parse(container.textContent); - let options = { - autoColumns: true, - layout: "fitColumns", - maxHeight: "200px" - }; - if (spec.options && Object.keys(spec.options).length > 0) { - options = spec.options; - } - const table = new Tabulator(container, options); - const tabulatorInstance = { id: container.id, spec, table, built: false }; - table.on("tableBuilt", () => { - table.off("tableBuilt"); - tabulatorInstance.built = true; - }); - tabulatorInstances.push(tabulatorInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "tabulator", index2, "parse", container); + const pluginName$4 = "slider"; + const className$4 = pluginClassName(pluginName$4); + const sliderPlugin = { + ...flaggableJsonPlugin(pluginName$4, className$4), + hydrateComponent: async (renderer, errorHandler, specs) => { + const sliderInstances = []; + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { continue; } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const html = \`
+
+ +
+
\`; + container.innerHTML = html; + const element = container.querySelector('input[type="range"]'); + const sliderInstance = { id: \`\${pluginName$4}-\${index2}\`, spec, element }; + sliderInstances.push(sliderInstance); } - const dataNameSelectedSuffix = defaultCommonOptions.dataNameSelectedSuffix; - const instances = tabulatorInstances.map((tabulatorInstance, index2) => { + const instances = sliderInstances.map((sliderInstance) => { var _a; + const { element, spec } = sliderInstance; + const valueSpan = (_a = element.parentElement) == null ? void 0 : _a.querySelector(".vega-bind-value"); const initialSignals = [{ - name: tabulatorInstance.spec.dataSignalName, - value: null, - priority: -1, - isData: true + name: spec.variableId, + value: spec.value || spec.min, + priority: 1, + isData: false }]; - if ((_a = tabulatorInstance.spec.options) == null ? void 0 : _a.selectableRows) { - initialSignals.push({ - name: \`\${tabulatorInstance.spec.dataSignalName}\${dataNameSelectedSuffix}\`, - value: [], - priority: -1, - isData: true - }); - } return { - ...tabulatorInstance, + ...sliderInstance, initialSignals, recieveBatch: async (batch) => { - var _a2; - const newData = (_a2 = batch[tabulatorInstance.spec.dataSignalName]) == null ? void 0 : _a2.value; - if (newData) { - if (!tabulatorInstance.built) { - tabulatorInstance.table.off("tableBuilt"); - tabulatorInstance.table.on("tableBuilt", () => { - tabulatorInstance.built = true; - tabulatorInstance.table.off("tableBuilt"); - tabulatorInstance.table.setData(newData); + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; + element.value = value.toString(); + if (valueSpan) { + valueSpan.textContent = value.toString(); + } + } + }, + beginListening() { + const updateValue = (e) => { + const value = parseFloat(e.target.value); + if (valueSpan) { + valueSpan.textContent = value.toString(); + } + const batch = { + [spec.variableId]: { + value, + isData: false + } + }; + renderer.signalBus.broadcast(sliderInstance.id, batch); + }; + element.addEventListener("input", updateValue); + element.addEventListener("change", updateValue); + }, + getCurrentSignalValue: () => { + return parseFloat(element.value); + }, + destroy: () => { + element.removeEventListener("input", sliderInstance.element.oninput); + element.removeEventListener("change", sliderInstance.element.onchange); + } + }; + }); + return instances; + } + }; + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + function inspectTabulatorSpec(spec) { + const flaggableSpec = { + spec + }; + return flaggableSpec; + } + const pluginName$3 = "tabulator"; + const className$3 = pluginClassName(pluginName$3); + const tabulatorPlugin = { + ...flaggableJsonPlugin(pluginName$3, className$3, inspectTabulatorSpec, { style: "box-sizing: border-box;" }), + hydrateComponent: async (renderer, errorHandler, specs) => { + const tabulatorInstances = []; + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + if (!Tabulator && index2 === 0) { + errorHandler(new Error("Tabulator not found"), pluginName$3, index2, "init", container); + continue; + } + const spec = specReview.approvedSpec; + if (!spec.dataSourceName || !spec.variableId) { + errorHandler(new Error("Tabulator requires dataSourceName and variableId"), pluginName$3, index2, "init", container); + continue; + } else if (spec.dataSourceName === spec.variableId) { + errorHandler(new Error("Tabulator dataSourceName and variableId cannot be the same"), pluginName$3, index2, "init", container); + continue; + } + let options = { + autoColumns: true, + layout: "fitColumns", + maxHeight: "200px" + }; + if (spec.tabulatorOptions && Object.keys(spec.tabulatorOptions).length > 0) { + options = spec.tabulatorOptions; + } + const table = new Tabulator(container, options); + const tabulatorInstance = { id: \`\${pluginName$3}-\${index2}\`, spec, table, built: false }; + table.on("tableBuilt", () => { + table.off("tableBuilt"); + tabulatorInstance.built = true; + }); + tabulatorInstances.push(tabulatorInstance); + } + const instances = tabulatorInstances.map((tabulatorInstance, index2) => { + var _a; + const initialSignals = [{ + name: tabulatorInstance.spec.dataSourceName, + value: null, + priority: -1, + isData: true + }]; + if ((_a = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a.selectableRows) { + initialSignals.push({ + name: tabulatorInstance.spec.variableId, + value: [], + priority: -1, + isData: true + }); + } + return { + ...tabulatorInstance, + initialSignals, + recieveBatch: async (batch) => { + var _a2; + const newData = (_a2 = batch[tabulatorInstance.spec.dataSourceName]) == null ? void 0 : _a2.value; + if (newData) { + if (!tabulatorInstance.built) { + tabulatorInstance.table.off("tableBuilt"); + tabulatorInstance.table.on("tableBuilt", () => { + tabulatorInstance.built = true; + tabulatorInstance.table.off("tableBuilt"); + tabulatorInstance.table.setData(newData); }); } else { tabulatorInstance.table.setData(newData); @@ -2032,15 +1942,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }, beginListening(sharedSignals) { var _a2; - if ((_a2 = tabulatorInstance.spec.options) == null ? void 0 : _a2.selectableRows) { + if ((_a2 = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a2.selectableRows) { for (const { isData, signalName } of sharedSignals) { if (isData) { - const matchData = signalName === \`\${tabulatorInstance.spec.dataSignalName}\${dataNameSelectedSuffix}\`; + const matchData = signalName === tabulatorInstance.spec.variableId; if (matchData) { tabulatorInstance.table.on("rowSelectionChanged", (e, rows) => { const selectedData = tabulatorInstance.table.getSelectedData(); const batch = { - [\`\${tabulatorInstance.spec.dataSignalName}\${dataNameSelectedSuffix}\`]: { + [tabulatorInstance.spec.variableId]: { value: selectedData, isData: true } @@ -2068,65 +1978,218 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ - const vegaLitePlugin = { - name: "vega-lite", - initializePlugin: (md) => definePlugin(md, "vega-lite"), - fence: (token, idx) => { - const vegaLiteId = \`vega-lite-\${idx}\`; - return sanitizedHTML("div", { id: vegaLiteId, class: "vega-chart" }, token.content.trim()); + const pluginName$2 = "textbox"; + const className$2 = pluginClassName(pluginName$2); + const textboxPlugin = { + ...flaggableJsonPlugin(pluginName$2, className$2), + hydrateComponent: async (renderer, errorHandler, specs) => { + const textboxInstances = []; + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const placeholderAttr = spec.placeholder ? \` placeholder="\${spec.placeholder}"\` : ""; + const inputElement = spec.multiline ? \`\` : \`\`; + const html = \`
+
+ +
+
\`; + container.innerHTML = html; + const element = container.querySelector(spec.multiline ? "textarea" : 'input[type="text"]'); + const textboxInstance = { id: \`\${pluginName$2}-\${index2}\`, spec, element }; + textboxInstances.push(textboxInstance); + } + const instances = textboxInstances.map((textboxInstance) => { + const { element, spec } = textboxInstance; + const initialSignals = [{ + name: spec.variableId, + value: spec.value || "", + priority: 1, + isData: false + }]; + return { + ...textboxInstance, + initialSignals, + recieveBatch: async (batch) => { + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; + element.value = value; + } + }, + beginListening() { + const updateValue = (e) => { + const value = e.target.value; + const batch = { + [spec.variableId]: { + value, + isData: false + } + }; + renderer.signalBus.broadcast(textboxInstance.id, batch); + }; + element.addEventListener("input", updateValue); + element.addEventListener("change", updateValue); + }, + getCurrentSignalValue: () => { + return element.value; + }, + destroy: () => { + element.removeEventListener("input", textboxInstance.element.oninput); + element.removeEventListener("change", textboxInstance.element.onchange); + } + }; + }); + return instances; } }; - async function resolveSpec(textContent) { - try { - const either = JSON.parse(textContent); - if (typeof either === "object") { - return resolveToVega(either); - } else { - return { error: new Error(\`Spec must be either a JSON object or a string url, found type \${typeof either}\`) }; + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + var LogLevel = /* @__PURE__ */ ((LogLevel2) => { + LogLevel2[LogLevel2["none"] = 0] = "none"; + LogLevel2[LogLevel2["some"] = 1] = "some"; + LogLevel2[LogLevel2["all"] = 2] = "all"; + return LogLevel2; + })(LogLevel || {}); + class SignalBus { + constructor(dataSignalPrefix) { + __publicField(this, "broadcastingStack"); + __publicField(this, "logLevel"); + __publicField(this, "logWatchIds"); + __publicField(this, "active"); + __publicField(this, "peers"); + __publicField(this, "signalDeps"); + __publicField(this, "peerDependencies"); + this.dataSignalPrefix = dataSignalPrefix; + this.logLevel = 0; + this.logWatchIds = []; + this.reset(); + } + log(id, message, ...optionalParams) { + if (this.logLevel === 0) return; + if (this.logWatchIds.length > 0 && !this.logWatchIds.includes(id)) return; + console.log(\`[Signal Bus][\${id}] \${message}\`, ...optionalParams); + } + async broadcast(originId, batch) { + if (this.broadcastingStack.includes(originId)) { + this.log(originId, "Additional broadcast from", originId, this.broadcastingStack.join(" -> ")); } - } catch (error) { - if (textContent.startsWith("http://") || textContent.startsWith("https://") || textContent.startsWith("//")) { - try { - const response = await fetch(textContent); - const either = await response.json(); - if (typeof either === "object") { - return resolveToVega(either); - } else { - return { error: new Error(\`Expected a JSON object, found type \${typeof either}\`) }; + this.log(originId, "Broadcasting batch from", originId, batch); + this.broadcastingStack.push(originId); + for (const peerId of this.peerDependencies[originId]) { + const peer = this.peers.find((p) => p.id === peerId); + if (!peer) continue; + const peerBatch = {}; + let hasBatch = false; + for (const signalName in batch) { + if (peer.initialSignals.some((s) => s.name === signalName) && batch[signalName].value !== this.signalDeps[signalName].value) { + peerBatch[signalName] = batch[signalName]; + hasBatch = true; } - } catch (error2) { - return { error: error2 }; } - } else { - return { error: new Error("Spec string must be a url") }; + if (!hasBatch) continue; + peer.recieveBatch && await peer.recieveBatch(peerBatch, originId); + } + this.broadcastingStack.pop(); + for (const signalName in batch) { + const signalDep = this.signalDeps[signalName]; + signalDep.value = batch[signalName].value; + } + if (this.broadcastingStack.length === 0) { + for (const peer of this.peers) { + peer.broadcastComplete && await peer.broadcastComplete(); + } } } - } - function resolveToVega(either) { - if ("$schema" in either && typeof either.$schema === "string") { - if (either.$schema.includes("vega-lite")) { - try { - const runtime = vegaLite.compile(either); - const { spec } = runtime; - return { spec }; - } catch (error) { - return { error }; + getPriorityPeer(signalName) { + const signalDep = this.signalDeps[signalName]; + if (!signalDep) return null; + return this.peers.find((p) => p.id === signalDep.initialPriorityId); + } + registerPeer(peer) { + this.log("registerPeer", "register", peer); + this.peers.push(peer); + for (const initialSignal of peer.initialSignals) { + if (!(initialSignal.name in this.signalDeps)) { + this.signalDeps[initialSignal.name] = { + deps: [peer], + priority: initialSignal.priority, + initialPriorityId: peer.id, + value: initialSignal.value, + isData: initialSignal.isData + }; + } else { + const signalDep = this.signalDeps[initialSignal.name]; + if (!signalDep.deps.includes(peer)) { + signalDep.deps.push(peer); + } + if (initialSignal.priority > signalDep.priority) { + signalDep.priority = initialSignal.priority; + signalDep.initialPriorityId = peer.id; + signalDep.value = initialSignal.value; + signalDep.isData = initialSignal.isData; + } } - } else if (either.$schema.includes("vega")) { - return { spec: either }; - } else { - return { error: new Error("$schema property must be a string with vega or vega-lite version.") }; } - } else { - return { error: new Error("Missing $schema property, must be a string with vega or vega-lite version.") }; } - } - function urlParam(urlParamName, value) { - if (value === void 0 || value === null) return ""; - if (Array.isArray(value)) { - return value.map((vn) => \`\${urlParamName}[]=\${encodeURIComponent(vn)}\`).join("&"); - } else { - return \`\${urlParamName}=\${encodeURIComponent(value)}\`; + beginListening() { + this.log("beginListening", "begin initial batch", this.signalDeps); + for (const peer of this.peers) { + const batch = {}; + for (const signalName in this.signalDeps) { + const signalDep = this.signalDeps[signalName]; + const { value, isData } = signalDep; + batch[signalName] = { value, isData }; + } + peer.recieveBatch && peer.recieveBatch(batch, "initial"); + } + this.log("beginListening", "end initial batch"); + const peerSignals = {}; + for (const signalName in this.signalDeps) { + const signalDep = this.signalDeps[signalName]; + if (signalDep.deps.length === 1) continue; + for (const peer of signalDep.deps) { + if (!(peer.id in peerSignals)) { + peerSignals[peer.id] = []; + this.peerDependencies[peer.id] = []; + } + peerSignals[peer.id].push({ signalName, isData: signalDep.isData }); + for (const otherPeer of signalDep.deps) { + if (otherPeer.id !== peer.id && !this.peerDependencies[peer.id].includes(otherPeer.id)) { + this.peerDependencies[peer.id].push(otherPeer.id); + } + } + } + } + this.log("beginListening", "======= dependencies =========", peerSignals, this.peerDependencies); + for (const peer of this.peers) { + const sharedSignals = peerSignals[peer.id]; + if (sharedSignals) { + this.log(peer.id, "Shared signals:", sharedSignals); + if (this.peerDependencies[peer.id]) { + this.log(peer.id, "Shared dependencies:", this.peerDependencies[peer.id]); + } + peer.beginListening && peer.beginListening(sharedSignals); + } else { + this.log(peer.id, "No shared signals"); + } + } + this.active = true; + } + reset() { + this.signalDeps = {}; + this.active = false; + this.peers = []; + this.broadcastingStack = []; + this.peerDependencies = {}; } } /*! @@ -2134,23 +2197,30 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Licensed under the MIT License. */ const ignoredSignals = ["width", "height", "padding", "autosize", "background", "style", "parent", "datum", "item", "event", "cursor"]; + const pluginName$1 = "vega"; + const className$1 = pluginClassName(pluginName$1); + function inspectVegaSpec(spec) { + const flaggableSpec = { + spec + }; + return flaggableSpec; + } const vegaPlugin = { - name: "vega", - initializePlugin: (md) => definePlugin(md, "vega"), - fence: (token, idx) => { - const vegaId = \`vega-\${idx}\`; - return sanitizedHTML("div", { id: vegaId, class: "vega-chart" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$1, className$1, inspectVegaSpec), + hydrateComponent: async (renderer, errorHandler, specs) => { if (!expressionsInitialized) { vega.expressionFunction("urlParam", urlParam); expressionsInitialized = true; } const vegaInstances = []; - const containers = renderer.element.querySelectorAll(".vega-chart"); const specInits = []; - for (const [index2, container] of Array.from(containers).entries()) { - const specInit = await createSpecInit(container, index2, renderer, errorHandler); + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const specInit = createSpecInit(container, index2, specReview.approvedSpec); if (specInit) { specInits.push(specInit); } @@ -2166,10 +2236,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy for (const vegaInstance of vegaInstances) { if (!vegaInstance.spec.data) continue; for (const data of vegaInstance.spec.data) { - const dataSignal = dataSignals.find( - (signal) => signal.name === data.name || \`\${signal.name}\${defaultCommonOptions.dataNameSelectedSuffix}\` === data.name - //match a selection from Tabulator - ); + const dataSignal = dataSignals.find((signal) => signal.name === data.name); if (dataSignal) { vegaInstance.initialSignals.push({ name: data.name, @@ -2328,30 +2395,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } return hasAnyChange; } - async function createSpecInit(container, index2, renderer, errorHandler) { + function createSpecInit(container, index2, spec) { var _a; - if (!container.textContent) { - container.innerHTML = '
Expected a spec object or a url
'; - return; - } - let result; - try { - result = await resolveSpec(container.textContent); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "vega", index2, "resolve", container); - return; - } - if (result.error) { - container.innerHTML = \`
\${result.error.toString()}
\`; - errorHandler(result.error, "vega", index2, "resolve", container); - return; - } - if (!result.spec) { - container.innerHTML = '
Expected a spec object
'; - return; - } - const { spec } = result; const initialSignals = ((_a = spec.signals) == null ? void 0 : _a.map((signal) => { if (ignoredSignals.includes(signal.name)) return; let isData = isSignalDataBridge(signal); @@ -2370,14 +2415,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } async function createVegaInstance(specInit, renderer, errorHandler) { const { container, index: index2, initialSignals, spec } = specInit; - const id = \`vega-\${index2}\`; + const id = \`\${pluginName$1}-\${index2}\`; let runtime; let view; try { runtime = vega.parse(spec); } catch (e) { container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "vega", index2, "parse", container); + errorHandler(e, pluginName$1, index2, "parse", container); return; } try { @@ -2385,7 +2430,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy container, renderer: renderer.options.vegaRenderer, logger: new VegaLogger((error) => { - errorHandler(error, "vega", index2, "view", container); + errorHandler(error, pluginName$1, index2, "view", container); }) }); view.run(); @@ -2399,7 +2444,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } } catch (e) { container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "vega", index2, "view", container); + errorHandler(e, pluginName$1, index2, "view", container); return; } const dataSignals = initialSignals.filter((signal) => { @@ -2473,6 +2518,46 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName = "vega-lite"; + const className = pluginClassName(pluginName); + const vegaLitePlugin = { + ...flaggableJsonPlugin(pluginName, className), + fence: (token, index2) => { + let json = token.content.trim(); + let spec; + let flaggableSpec; + try { + spec = JSON.parse(json); + } catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reasons: [\`malformed JSON\`] + }; + } + if (spec) { + try { + const vegaSpec = vegaLite.compile(spec); + flaggableSpec = inspectVegaSpec(vegaSpec.spec); + } catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reasons: [\`failed to compile vega spec\`] + }; + } + } + if (flaggableSpec) { + json = JSON.stringify(flaggableSpec); + } + return sanitizedHTML("div", { class: pluginClassName(vegaPlugin.name), id: \`\${pluginName}-\${index2}\` }, json, true); + }, + hydratesBefore: vegaPlugin.name + }; + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ function registerNativePlugins() { registerMarkdownPlugin(checkboxPlugin); registerMarkdownPlugin(cssPlugin); @@ -2480,7 +2565,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy registerMarkdownPlugin(imagePlugin); registerMarkdownPlugin(placeholdersPlugin); registerMarkdownPlugin(presetsPlugin); + registerMarkdownPlugin(sliderPlugin); registerMarkdownPlugin(tabulatorPlugin); + registerMarkdownPlugin(textboxPlugin); registerMarkdownPlugin(vegaLitePlugin); registerMarkdownPlugin(vegaPlugin); } @@ -2488,6 +2575,111 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const defaultRendererOptions = { + vegaRenderer: "canvas", + useShadowDom: false, + errorHandler: (error, pluginName2, instanceIndex, phase) => { + console.error(\`Error in plugin \${pluginName2} instance \${instanceIndex} phase \${phase}\`, error); + } + }; + class Renderer { + constructor(_element, options) { + __publicField(this, "md"); + __publicField(this, "instances"); + __publicField(this, "signalBus"); + __publicField(this, "options"); + __publicField(this, "shadowRoot"); + __publicField(this, "element"); + this.options = { ...defaultRendererOptions, ...options }; + this.signalBus = this.options.signalBus || new SignalBus(defaultCommonOptions.dataSignalPrefix); + this.instances = {}; + if (this.options.useShadowDom) { + this.shadowRoot = _element.attachShadow({ mode: "open" }); + this.element = this.shadowRoot; + } else { + this.element = _element; + } + } + ensureMd() { + if (!this.md) { + this.md = create(); + } + } + async render(markdown) { + this.reset(); + const html = this.renderHtml(markdown); + this.element.innerHTML = html; + const specs = this.hydrateSpecs(); + await this.hydrate(specs); + } + renderHtml(markdown) { + this.ensureMd(); + const parsedHTML = this.md.render(markdown); + let content = parsedHTML; + if (this.options.useShadowDom) { + content = \`
\${content}
\`; + } + return content; + } + hydrateSpecs() { + this.ensureMd(); + const specs = []; + this.signalBus.log("Renderer", "hydrate specs"); + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + if (plugin.hydrateSpecs) { + specs.push(...plugin.hydrateSpecs(this, this.options.errorHandler)); + } + } + return specs; + } + async hydrate(specs) { + this.ensureMd(); + this.signalBus.log("Renderer", "hydrate components"); + const hydrationPromises = []; + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + if (plugin.hydrateComponent) { + const specsForPlugin = specs.filter((spec) => spec.pluginName === plugin.name); + hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler, specsForPlugin).then((instances) => { + return { + pluginName: plugin.name, + instances + }; + })); + } + } + try { + const pluginHydrations = await Promise.all(hydrationPromises); + for (const hydration of pluginHydrations) { + if (hydration && hydration.instances) { + this.instances[hydration.pluginName] = hydration.instances; + for (const instance of hydration.instances) { + this.signalBus.registerPeer(instance); + } + } + } + this.signalBus.beginListening(); + } catch (error) { + console.error("Error in rendering plugins", error); + } + } + reset() { + this.signalBus.reset(); + for (const pluginName2 of Object.keys(this.instances)) { + const instances = this.instances[pluginName2]; + for (const instance of instances) { + instance.destroy && instance.destroy(); + } + } + this.instances = {}; + this.element.innerHTML = ""; + } + } + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ registerNativePlugins(); const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -2502,8 +2694,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); }); `; - const sandboxedJs = `document.addEventListener('DOMContentLoaded', () => { - const renderer = new IDocs.markdown.Renderer(document.body, { + const sandboxedJs = `let renderer; +document.addEventListener('DOMContentLoaded', () => { + let transactionIndex = 0; + const transactions = {}; + renderer = new IDocs.markdown.Renderer(document.body, { errorHandler: (error, pluginName, instanceIndex, phase, container, detail) => { console.error(\`Error in plugin \${pluginName} at instance \${instanceIndex} during \${phase}:\`, error); if (detail) { @@ -2515,12 +2710,19 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy function render(request) { if (request.markdown) { renderer.reset(); + //debugger; const html = renderer.renderHtml(request.markdown); - //todo: look at dom elements prior to hydration renderer.element.innerHTML = html; - //todo: send message to parent to ask for whitelist - //todo: asynchronously hydrate the renderer - renderer.hydrate(); + const specs = renderer.hydrateSpecs(); + const transactionId = transactionIndex++; + transactions[transactionId] = specs; + //send message to parent to ask for whitelist + const sandboxedPreRenderMessage = { + type: 'sandboxedPreHydrate', + transactionId, + specs, + }; + window.parent.postMessage(sandboxedPreRenderMessage, '*'); } } render(renderRequest); @@ -2528,15 +2730,42 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy window.addEventListener('message', (event) => { if (!event.data) return; - render(event.data); + const message = event.data; + switch (message.type) { + case 'sandboxRender': { + render(message); + break; + } + case 'sandboxApproval': { + //debugger; + //only handle if the transactionId is the latest + if (message.transactionId === transactionIndex - 1) { + //todo: console.warn of unapproved + //hydrate the renderer + const flags = transactions[message.transactionId]; + if (flags) { + renderer.hydrate(flags); + } + } + else { + console.debug('Received sandbox approval for an outdated transaction:', message.transactionId, transactionIndex); + } + break; + } + } }); }); `; class Sandbox extends Previewer { constructor(elementOrSelector, markdown, options) { super(elementOrSelector, markdown, options); + __publicField(this, "options"); __publicField(this, "iframe"); - const renderRequest = { markdown }; + this.options = options; + const renderRequest = { + type: "sandboxRender", + markdown + }; const { iframe } = createIframe(this.getDependencies(), renderRequest); this.iframe = iframe; this.element.appendChild(this.iframe); @@ -2549,6 +2778,21 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy console.error("Error loading iframe:", error); (_a = options == null ? void 0 : options.onError) == null ? void 0 : _a.call(options, new Error("Failed to load iframe")); }); + window.addEventListener("message", (event) => { + var _a; + if (event.source === this.iframe.contentWindow) { + const message = event.data; + if (message.type == "sandboxedPreHydrate") { + const specs = this.options.onApprove(message); + const sandboxedApprovalMessage = { + type: "sandboxApproval", + transactionId: message.transactionId, + specs + }; + (_a = this.iframe.contentWindow) == null ? void 0 : _a.postMessage(sandboxedApprovalMessage, "*"); + } + } + }); } destroy() { var _a; @@ -2560,7 +2804,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } send(markdown) { var _a; - (_a = this.iframe.contentWindow) == null ? void 0 : _a.postMessage({ markdown }, "*"); + const message = { + type: "sandboxRender", + markdown + }; + (_a = this.iframe.contentWindow) == null ? void 0 : _a.postMessage(message, "*"); } getDependencies() { return ` @@ -2619,7 +2867,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy this.pendingUpdate = null; } }, - onError: (error) => console.error("Sandbox initialization failed:", error) + onError: (error) => console.error("Sandbox initialization failed:", error), + onApprove: this.props.onApprove } ); } catch (error) { @@ -2691,7 +2940,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy React.useEffect(() => { const handleMessage = (event) => { if (event.data && event.data.sender !== "editor") { - if (event.data.type === "page" && event.data.page) { + if (event.data.type === "editorPage" && event.data.page) { setPage(event.data.page); } } @@ -2703,18 +2952,26 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }, []); React.useEffect(() => { const readyMessage = { - type: "ready", + type: "editorReady", sender: "editor" }; postMessageTarget.postMessage(readyMessage, "*"); }, []); - return /* @__PURE__ */ React.createElement(EditorView, { page, postMessageTarget, previewer: props.previewer }); + return /* @__PURE__ */ React.createElement( + EditorView, + { + page, + postMessageTarget, + previewer: props.previewer, + onApprove: props.onApprove + } + ); } function EditorView(props) { - const { page, postMessageTarget, previewer } = props; + const { page, postMessageTarget, previewer, onApprove } = props; const sendEditToApp = (newPage) => { const pageMessage = { - type: "page", + type: "editorPage", page: newPage, sender: "editor" }; @@ -2792,7 +3049,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy SandboxDocumentPreview, { page, - previewer + previewer, + onApprove } ))); } @@ -2826,7 +3084,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return; } const pageMessage = { - type: "page", + type: "editorPage", page, sender: "app" }; @@ -2835,10 +3093,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy React.useEffect(() => { const handleMessage = (event) => { if (event.data && event.data.sender === "editor") { - if (event.data.type === "ready") { + if (event.data.type === "editorReady") { setIsEditorReady(true); sendPageToEditor(currentPage); - } else if (event.data.type === "page" && event.data.page) { + } else if (event.data.type === "editorPage" && event.data.page) { const pageMessage = event.data; setHistoryIndex((prevIndex) => { setHistory((prevHistory) => { @@ -2903,7 +3161,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy ), /* @__PURE__ */ React.createElement("span", { style: { marginLeft: "10px", fontSize: "12px", color: "#666" } }, "History: ", historyIndex + 1, " / ", history.length))), /* @__PURE__ */ React.createElement("div", { ref: editorContainerRef, style: { flex: 1 } }, /* @__PURE__ */ React.createElement( Editor, { - previewer + previewer, + onApprove: props.onApprove } ))); } @@ -2929,7 +3188,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy { "type": "table", "dataSourceName": "seattle_weather", - "options": {} + "variableId": "seattle_weather_selected", + "tabulatorOptions": {} }, "Here is a stacked bar chart of Seattle weather:\nEach bar represents the count of weather types for each month.\nThe colors distinguish between different weather conditions such as sun, fog, drizzle, rain, and snow.", { diff --git a/docs/dist/idocs.host.umd.js b/docs/dist/idocs.host.umd.js index 9af57e42..f549ca02 100644 --- a/docs/dist/idocs.host.umd.js +++ b/docs/dist/idocs.host.umd.js @@ -6,7 +6,6 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const defaultCommonOptions = { - dataNameSelectedSuffix: "_selected", dataSignalPrefix: "data_signal:", groupClassName: "group" }; @@ -18,11 +17,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return name.replace(/[^a-zA-Z0-9_]/g, "_"); } function getChartType(spec) { - const $schema2 = spec == null ? void 0 : spec.$schema; - if (!$schema2) { + const $schema = spec == null ? void 0 : spec.$schema; + if (!$schema) { return "vega-lite"; } - return $schema2.includes("vega-lite") ? "vega-lite" : "vega"; + return $schema.includes("vega-lite") ? "vega-lite" : "vega"; } function changePageOrigin(page, oldOrigin, newOriginUrl) { const newPage = { @@ -58,19 +57,104 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }; return newPage; } - function addStaticDataLoaderToSpec(vegaScope, dataSource) { - const { spec } = vegaScope; - const { dataSourceName } = dataSource; - if (!spec.signals) { - spec.signals = []; + function topologicalSort(list) { + var _a; + const nameToObject = /* @__PURE__ */ new Map(); + const inDegree = /* @__PURE__ */ new Map(); + const graph = /* @__PURE__ */ new Map(); + for (const obj of list) { + nameToObject.set(obj.variableId, obj); + inDegree.set(obj.variableId, 0); + graph.set(obj.variableId, []); } - spec.signals.push({ - name: dataSourceName, - update: `data('${dataSourceName}')` + for (const obj of list) { + const sources = ((_a = obj.calculation) == null ? void 0 : _a.dependsOn) || []; + for (const dep of sources) { + if (!graph.has(dep)) { + continue; + } + graph.get(dep).push(obj.variableId); + inDegree.set(obj.variableId, inDegree.get(obj.variableId) + 1); + } + } + const queue = []; + for (const [name, degree] of inDegree.entries()) { + if (degree === 0) + queue.push(name); + } + const sorted = []; + while (queue.length) { + const current = queue.shift(); + sorted.push(nameToObject.get(current)); + for (const neighbor of graph.get(current)) { + inDegree.set(neighbor, inDegree.get(neighbor) - 1); + if (inDegree.get(neighbor) === 0) { + queue.push(neighbor); + } + } + } + if (sorted.length !== list.length) { + throw new Error("Cycle or missing dependency detected"); + } + return sorted; + } + function createSpecWithVariables(variables, tableElements, stubDataLoaders) { + const spec = { + $schema: "https://vega.github.io/schema/vega/v5.json", + signals: [], + data: [] + }; + tableElements.forEach((table) => { + const { variableId } = table; + spec.signals.push(dataAsSignal(variableId)); + spec.data.unshift({ + name: variableId, + values: [] + }); }); + topologicalSort(variables).forEach((v) => { + if (isDataframePipeline(v)) { + const { dataFrameTransformations } = v.calculation; + const data = { + name: v.variableId, + source: v.calculation.dependsOn || [], + transform: dataFrameTransformations + }; + spec.data.push(data); + spec.signals.push(dataAsSignal(v.variableId)); + } else { + const signal = { name: v.variableId, value: v.initialValue }; + if (v.calculation) { + signal.update = v.calculation.vegaExpression; + } + spec.signals.push(signal); + } + }); + return spec; + } + function isDataframePipeline(variable) { + var _a, _b; + return variable.type === "object" && !!variable.isArray && (((_a = variable.calculation) == null ? void 0 : _a.dependsOn) !== void 0 && variable.calculation.dependsOn.length > 0 || ((_b = variable.calculation) == null ? void 0 : _b.dataFrameTransformations) !== void 0 && variable.calculation.dataFrameTransformations.length > 0); + } + function ensureDataAndSignalsArray(spec) { if (!spec.data) { spec.data = []; } + if (!spec.signals) { + spec.signals = []; + } + } + function dataAsSignal(name) { + return { + name, + update: `data('${name}')` + }; + } + function addStaticDataLoaderToSpec(vegaScope, dataSource) { + const { spec } = vegaScope; + const { dataSourceName } = dataSource; + ensureDataAndSignalsArray(spec); + spec.signals.push(dataAsSignal(dataSourceName)); const newData = { name: dataSourceName, values: [], @@ -91,16 +175,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const { dataSourceName } = dataSource; const urlSignal = vegaScope.createUrlSignal(dataSource.urlRef); const url = { signal: urlSignal.name }; - if (!spec.signals) { - spec.signals = []; - } - spec.signals.push({ - name: dataSourceName, - update: `data('${dataSourceName}')` - }); - if (!spec.data) { - spec.data = []; - } + ensureDataAndSignalsArray(spec); + spec.signals.push(dataAsSignal(dataSourceName)); spec.data.unshift({ name: dataSourceName, url, @@ -153,83 +229,6 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return JSON.stringify(param.value); } } - function topologicalSort(list) { - var _a; - const nameToObject = /* @__PURE__ */ new Map(); - const inDegree = /* @__PURE__ */ new Map(); - const graph = /* @__PURE__ */ new Map(); - for (const obj of list) { - nameToObject.set(obj.variableId, obj); - inDegree.set(obj.variableId, 0); - graph.set(obj.variableId, []); - } - for (const obj of list) { - const sources = ((_a = obj.calculation) == null ? void 0 : _a.dependsOn) || []; - for (const dep of sources) { - if (!graph.has(dep)) { - continue; - } - graph.get(dep).push(obj.variableId); - inDegree.set(obj.variableId, inDegree.get(obj.variableId) + 1); - } - } - const queue = []; - for (const [name, degree] of inDegree.entries()) { - if (degree === 0) - queue.push(name); - } - const sorted = []; - while (queue.length) { - const current = queue.shift(); - sorted.push(nameToObject.get(current)); - for (const neighbor of graph.get(current)) { - inDegree.set(neighbor, inDegree.get(neighbor) - 1); - if (inDegree.get(neighbor) === 0) { - queue.push(neighbor); - } - } - } - if (sorted.length !== list.length) { - throw new Error("Cycle or missing dependency detected"); - } - return sorted; - } - function createSpecWithVariables(dataNameSelectedSuffix, variables, stubDataLoaders) { - const spec = { - $schema: "https://vega.github.io/schema/vega/v5.json", - signals: [], - data: [] - }; - topologicalSort(variables).forEach((v) => { - if (isDataframePipeline(v)) { - const { dataFrameTransformations } = v.calculation; - const data = { - name: v.variableId, - source: v.calculation.dependsOn || [], - transform: dataFrameTransformations - }; - spec.data.push(data); - if (!spec.signals) { - spec.signals = []; - } - spec.signals.push({ - name: v.variableId, - update: `data('${v.variableId}')` - }); - } else { - const signal = { name: v.variableId, value: v.initialValue }; - if (v.calculation) { - signal.update = v.calculation.vegaExpression; - } - spec.signals.push(signal); - } - }); - return spec; - } - function isDataframePipeline(variable) { - var _a, _b; - return variable.type === "object" && !!variable.isArray && (((_a = variable.calculation) == null ? void 0 : _a.dependsOn) !== void 0 && variable.calculation.dependsOn.length > 0 || ((_b = variable.calculation) == null ? void 0 : _b.dataFrameTransformations) !== void 0 && variable.calculation.dataFrameTransformations.length > 0); - } function tickWrap(tick, content) { return `\`\`\`${tick} ${content} @@ -247,7 +246,6 @@ ${content} ${content} :::`; } - const $schema = "https://vega.github.io/schema/vega/v5.json"; function targetMarkdown(page) { var _a; const mdSections = []; @@ -256,7 +254,8 @@ ${content} if ((_a = page.layout) == null ? void 0 : _a.css) { mdSections.push(tickWrap("css", page.layout.css)); } - const vegaScope = dataLoaderMarkdown(dataLoaders.filter((dl) => dl.type !== "spec"), variables); + const tableElements = page.groups.flatMap((group) => group.elements.filter((e) => typeof e !== "string" && e.type === "table")); + const vegaScope = dataLoaderMarkdown(dataLoaders.filter((dl) => dl.type !== "spec"), variables, tableElements); for (const dataLoader of dataLoaders.filter((dl) => dl.type === "spec")) { mdSections.push(chartWrap(dataLoader.spec)); } @@ -267,8 +266,8 @@ ${content} const markdown = mdSections.join("\n\n"); return markdown; } - function dataLoaderMarkdown(dataSources, variables) { - const spec = createSpecWithVariables(defaultCommonOptions.dataNameSelectedSuffix, variables); + function dataLoaderMarkdown(dataSources, variables, tableElements) { + const spec = createSpecWithVariables(variables, tableElements); const vegaScope = new VegaScope(spec); for (const dataSource of dataSources) { switch (dataSource.type) { @@ -285,12 +284,6 @@ ${content} break; } } - if (!spec.data) { - spec.data = []; - } - spec.data.unshift({ - name: dataSource.dataSourceName + defaultCommonOptions.dataNameSelectedSuffix - }); } return vegaScope; } @@ -313,7 +306,7 @@ ${content} } case "checkbox": { const cbSpec = { - name: element.variableId, + variableId: element.variableId, value: (_a = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _a.initialValue, label: element.label }; @@ -322,13 +315,13 @@ ${content} } case "dropdown": { const ddSpec = { - name: element.variableId, + variableId: element.variableId, value: (_b = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _b.initialValue, label: element.label }; if (element.dynamicOptions) { ddSpec.dynamicOptions = { - dataSignalName: element.dynamicOptions.dataSourceName, + dataSourceName: element.dynamicOptions.dataSourceName, fieldName: element.dynamicOptions.fieldName }; } else { @@ -358,48 +351,32 @@ ${content} break; } case "slider": { - const spec = { - $schema, - signals: [ - { - name: element.variableId, - value: (_c = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _c.initialValue, - bind: { - input: "range", - min: element.min, - max: element.max, - step: element.step, - debounce: 100 - } - } - ] + const sliderSpec = { + variableId: element.variableId, + value: (_c = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _c.initialValue, + label: element.label, + min: element.min, + max: element.max, + step: element.step }; - mdElements.push(chartWrap(spec)); + mdElements.push(jsonWrap("slider", JSON.stringify(sliderSpec, null, 2))); break; } case "table": { - const tableSpec = { - dataSignalName: element.dataSourceName, - options: element.options - }; + const { dataSourceName, variableId, tabulatorOptions } = element; + const tableSpec = { dataSourceName, variableId, tabulatorOptions }; mdElements.push(jsonWrap("tabulator", JSON.stringify(tableSpec, null, 2))); break; } case "textbox": { - const spec = { - $schema, - signals: [ - { - name: element.variableId, - value: (_d = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _d.initialValue, - bind: { - input: "text", - debounce: 100 - } - } - ] + const textboxElement = element; + const textboxSpec = { + variableId: textboxElement.variableId, + value: (_d = variables.find((v) => v.variableId === textboxElement.variableId)) == null ? void 0 : _d.initialValue, + label: textboxElement.label, + multiline: textboxElement.multiline }; - mdElements.push(chartWrap(spec)); + mdElements.push(jsonWrap("textbox", JSON.stringify(textboxSpec, null, 2))); break; } } @@ -462,7 +439,6 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const defaultCommonOptions = { - dataNameSelectedSuffix: "_selected", dataSignalPrefix: "data_signal:", groupClassName: "group" }; @@ -771,7 +747,21 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy */ const plugins = []; function registerMarkdownPlugin(plugin) { - plugins.push(plugin); + let insertIndex = plugins.length; + let minIndex = 0; + for (let i = 0; i < plugins.length; i++) { + if (plugins[i].hydratesBefore === plugin.name) { + minIndex = Math.max(minIndex, i + 1); + } + } + if (plugin.hydratesBefore) { + const targetIndex = plugins.findIndex((p) => p.name === plugin.hydratesBefore); + if (targetIndex !== -1) { + insertIndex = targetIndex; + } + } + insertIndex = Math.max(insertIndex, minIndex); + plugins.splice(insertIndex, 0, plugin); return "register"; } function create() { @@ -787,328 +777,175 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const token = tokens[idx]; const info = token.info.trim(); if (info.startsWith("json ")) { - const pluginName = info.slice(5).trim(); - const plugin = plugins.find((p) => p.name === pluginName); + const pluginName2 = info.slice(5).trim(); + const plugin = plugins.find((p) => p.name === pluginName2); if (plugin && plugin.fence) { - return plugin.fence(token, idx); - } - } - if (originalFence) { - return originalFence(tokens, idx, options, env, slf); - } else { - return ""; - } - }; - return md; - } - function definePlugin(md, pluginName) { - md.block.ruler.before("fence", \`\${pluginName}_block\`, function(state, startLine, endLine) { - const start = state.bMarks[startLine] + state.tShift[startLine]; - const max = state.eMarks[startLine]; - const marker = \`json \${pluginName}\`; - if (!state.src.slice(start, max).trim().startsWith("\`\`\`" + marker)) { - return false; - } - let nextLine = startLine; - while (nextLine < endLine) { - nextLine++; - if (state.src.slice(state.bMarks[nextLine] + state.tShift[nextLine], state.eMarks[nextLine]).trim() === "\`\`\`") { - break; - } - } - state.line = nextLine + 1; - const token = state.push("fence", "code", 0); - token.info = marker; - token.content = state.getLines(startLine + 1, nextLine, state.blkIndent, true); - token.map = [startLine, state.line]; - return true; - }); - } - /*! - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ - var LogLevel = /* @__PURE__ */ ((LogLevel2) => { - LogLevel2[LogLevel2["none"] = 0] = "none"; - LogLevel2[LogLevel2["some"] = 1] = "some"; - LogLevel2[LogLevel2["all"] = 2] = "all"; - return LogLevel2; - })(LogLevel || {}); - class SignalBus { - constructor(dataSignalPrefix) { - __publicField(this, "broadcastingStack"); - __publicField(this, "logLevel"); - __publicField(this, "logWatchIds"); - __publicField(this, "active"); - __publicField(this, "peers"); - __publicField(this, "signalDeps"); - __publicField(this, "peerDependencies"); - this.dataSignalPrefix = dataSignalPrefix; - this.logLevel = 0; - this.logWatchIds = []; - this.reset(); - } - log(id, message, ...optionalParams) { - if (this.logLevel === 0) return; - if (this.logWatchIds.length > 0 && !this.logWatchIds.includes(id)) return; - console.log(\`[Signal Bus][\${id}] \${message}\`, ...optionalParams); - } - async broadcast(originId, batch) { - if (this.broadcastingStack.includes(originId)) { - this.log(originId, "Additional broadcast from", originId, this.broadcastingStack.join(" -> ")); - } - this.log(originId, "Broadcasting batch from", originId, batch); - this.broadcastingStack.push(originId); - for (const peerId of this.peerDependencies[originId]) { - const peer = this.peers.find((p) => p.id === peerId); - if (!peer) continue; - const peerBatch = {}; - let hasBatch = false; - for (const signalName in batch) { - if (peer.initialSignals.some((s) => s.name === signalName) && batch[signalName].value !== this.signalDeps[signalName].value) { - peerBatch[signalName] = batch[signalName]; - hasBatch = true; - } - } - if (!hasBatch) continue; - peer.recieveBatch && await peer.recieveBatch(peerBatch, originId); - } - this.broadcastingStack.pop(); - for (const signalName in batch) { - const signalDep = this.signalDeps[signalName]; - signalDep.value = batch[signalName].value; - } - if (this.broadcastingStack.length === 0) { - for (const peer of this.peers) { - peer.broadcastComplete && await peer.broadcastComplete(); - } - } - } - getPriorityPeer(signalName) { - const signalDep = this.signalDeps[signalName]; - if (!signalDep) return null; - return this.peers.find((p) => p.id === signalDep.initialPriorityId); - } - registerPeer(peer) { - this.peers.push(peer); - for (const initialSignal of peer.initialSignals) { - if (!(initialSignal.name in this.signalDeps)) { - this.signalDeps[initialSignal.name] = { - deps: [peer], - priority: initialSignal.priority, - initialPriorityId: peer.id, - value: initialSignal.value, - isData: initialSignal.isData - }; - } else { - const signalDep = this.signalDeps[initialSignal.name]; - if (!signalDep.deps.includes(peer)) { - signalDep.deps.push(peer); - } - if (initialSignal.priority > signalDep.priority) { - signalDep.priority = initialSignal.priority; - signalDep.initialPriorityId = peer.id; - signalDep.value = initialSignal.value; - signalDep.isData = initialSignal.isData; - } - } - } - } - beginListening() { - this.log("beginListening", "begin initial batch", this.signalDeps); - for (const peer of this.peers) { - const batch = {}; - for (const signalName in this.signalDeps) { - const signalDep = this.signalDeps[signalName]; - const { value, isData } = signalDep; - batch[signalName] = { value, isData }; - } - peer.recieveBatch && peer.recieveBatch(batch, "initial"); - } - this.log("beginListening", "end initial batch"); - const peerSignals = {}; - for (const signalName in this.signalDeps) { - const signalDep = this.signalDeps[signalName]; - if (signalDep.deps.length === 1) continue; - for (const peer of signalDep.deps) { - if (!(peer.id in peerSignals)) { - peerSignals[peer.id] = []; - this.peerDependencies[peer.id] = []; - } - peerSignals[peer.id].push({ signalName, isData: signalDep.isData }); - for (const otherPeer of signalDep.deps) { - if (otherPeer.id !== peer.id && !this.peerDependencies[peer.id].includes(otherPeer.id)) { - this.peerDependencies[peer.id].push(otherPeer.id); - } - } - } - } - this.log("beginListening", "======= dependencies =========", peerSignals, this.peerDependencies); - for (const peer of this.peers) { - const sharedSignals = peerSignals[peer.id]; - if (sharedSignals) { - this.log(peer.id, "Shared signals:", sharedSignals); - if (this.peerDependencies[peer.id]) { - this.log(peer.id, "Shared dependencies:", this.peerDependencies[peer.id]); - } - peer.beginListening && peer.beginListening(sharedSignals); - } else { - this.log(peer.id, "No shared signals"); - } - } - this.active = true; - } - reset() { - this.signalDeps = {}; - this.active = false; - this.peers = []; - this.broadcastingStack = []; - this.peerDependencies = {}; - } - } - /*! - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ - const defaultRendererOptions = { - vegaRenderer: "canvas", - useShadowDom: false, - errorHandler: (error, pluginName, instanceIndex, phase) => { - console.error(\`Error in plugin \${pluginName} instance \${instanceIndex} phase \${phase}\`, error); - } - }; - class Renderer { - constructor(_element, options) { - __publicField(this, "md"); - __publicField(this, "instances"); - __publicField(this, "signalBus"); - __publicField(this, "options"); - __publicField(this, "shadowRoot"); - __publicField(this, "element"); - this.options = { ...defaultRendererOptions, ...options }; - this.signalBus = this.options.signalBus || new SignalBus(defaultCommonOptions.dataSignalPrefix); - this.instances = {}; - if (this.options.useShadowDom) { - this.shadowRoot = _element.attachShadow({ mode: "open" }); - this.element = this.shadowRoot; - } else { - this.element = _element; - } - } - ensureMd() { - if (!this.md) { - this.md = create(); - } - } - async render(markdown) { - await this.reset(); - const content = this.renderHtml(markdown); - this.element.innerHTML = content; - await this.hydrate(); - } - renderHtml(markdown) { - this.ensureMd(); - const parsedHTML = this.md.render(markdown); - let content = parsedHTML; - if (this.options.useShadowDom) { - content = \`
\${content}
\`; - } - return content; - } - async hydrate() { - this.ensureMd(); - this.signalBus.log("Renderer", "rendering DOM"); - const hydrationPromises = []; - for (let i = 0; i < plugins.length; i++) { - const plugin = plugins[i]; - if (plugin.hydrateComponent) { - hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler).then((instances) => { - return { - pluginName: plugin.name, - instances - }; - })); + return plugin.fence(token, idx); } } - try { - const pluginHydrations = await Promise.all(hydrationPromises); - for (const hydration of pluginHydrations) { - if (hydration && hydration.instances) { - this.instances[hydration.pluginName] = hydration.instances; - for (const instance of hydration.instances) { - this.signalBus.registerPeer(instance); - } - } - } - this.signalBus.beginListening(); - } catch (error) { - console.error("Error in rendering plugins", error); + if (originalFence) { + return originalFence(tokens, idx, options, env, slf); + } else { + return ""; } - } - reset() { - this.signalBus.reset(); - for (const pluginName of Object.keys(this.instances)) { - const instances = this.instances[pluginName]; - for (const instance of instances) { - instance.destroy && instance.destroy(); + }; + return md; + } + function definePlugin(md, pluginName2) { + md.block.ruler.before("fence", \`\${pluginName2}_block\`, function(state, startLine, endLine) { + const start = state.bMarks[startLine] + state.tShift[startLine]; + const max = state.eMarks[startLine]; + const marker = \`json \${pluginName2}\`; + if (!state.src.slice(start, max).trim().startsWith("\`\`\`" + marker)) { + return false; + } + let nextLine = startLine; + while (nextLine < endLine) { + nextLine++; + if (state.src.slice(state.bMarks[nextLine] + state.tShift[nextLine], state.eMarks[nextLine]).trim() === "\`\`\`") { + break; } } - this.instances = {}; - this.element.innerHTML = ""; + state.line = nextLine + 1; + const token = state.push("fence", "code", 0); + token.info = marker; + token.content = state.getLines(startLine + 1, nextLine, state.blkIndent, true); + token.map = [startLine, state.line]; + return true; + }); + } + function urlParam(urlParamName, value) { + if (value === void 0 || value === null) return ""; + if (Array.isArray(value)) { + return value.map((vn) => \`\${urlParamName}[]=\${encodeURIComponent(vn)}\`).join("&"); + } else { + return \`\${urlParamName}=\${encodeURIComponent(value)}\`; + } + } + function getJsonScriptTag(container, errorHandler) { + const scriptTag = container.previousElementSibling; + if ((scriptTag == null ? void 0 : 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 { + return JSON.parse(scriptTag.textContent); + } catch (error) { + errorHandler(error); + return null; } } + function pluginClassName(pluginName2) { + return \`chartifact-plugin-\${pluginName2}\`; + } /*! * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ - function sanitizedHTML(tagName, attributes, content) { + function sanitizedHTML(tagName, attributes, content, precedeWithScriptTag) { const element = document.createElement(tagName); Object.keys(attributes).forEach((key) => { element.setAttribute(key, attributes[key]); }); - element.textContent = content; + if (precedeWithScriptTag) { + const scriptElement = document.createElement("script"); + scriptElement.setAttribute("type", "application/json"); + const safeContent = content.replace(/<\\/script>/gi, "<\\\\/script>"); + scriptElement.innerHTML = safeContent; + return scriptElement.outerHTML + element.outerHTML; + } else { + element.textContent = content; + } return element.outerHTML; } + function flaggableJsonPlugin(pluginName2, className2, flagger, attrs) { + const plugin = { + name: pluginName2, + initializePlugin: (md) => definePlugin(md, pluginName2), + fence: (token, index2) => { + let json = token.content.trim(); + let spec; + let flaggableSpec; + try { + spec = JSON.parse(json); + } catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reasons: [\`malformed JSON\`] + }; + } + if (spec) { + if (flagger) { + flaggableSpec = flagger(spec); + } else { + flaggableSpec = { spec }; + } + } + if (flaggableSpec) { + json = JSON.stringify(flaggableSpec); + } + return sanitizedHTML("div", { class: className2, id: \`\${pluginName2}-\${index2}\`, ...attrs }, json, true); + }, + hydrateSpecs: (renderer, errorHandler) => { + var _a; + const flagged = []; + const containers = renderer.element.querySelectorAll(\`.\${className2}\`); + for (const [index2, container] of Array.from(containers).entries()) { + const flaggableSpec = getJsonScriptTag(container, (e) => errorHandler(e, pluginName2, index2, "parse", container)); + if (!flaggableSpec) continue; + const f = { approvedSpec: null, pluginName: pluginName2, containerId: container.id }; + if (flaggableSpec.hasFlags) { + f.blockedSpec = flaggableSpec.spec; + f.reason = ((_a = flaggableSpec.reasons) == null ? void 0 : _a.join(", ")) || "Unknown reason"; + } else { + f.approvedSpec = flaggableSpec.spec; + } + flagged.push(f); + } + return flagged; + } + }; + return plugin; + } /*! * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$a = "checkbox"; + const className$9 = pluginClassName(pluginName$a); const checkboxPlugin = { - name: "checkbox", - initializePlugin: (md) => definePlugin(md, "checkbox"), - fence: (token, idx) => { - const CheckboxId = \`Checkbox-\${idx}\`; - return sanitizedHTML("div", { id: CheckboxId, class: "checkbox" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$a, className$9), + hydrateComponent: async (renderer, errorHandler, specs) => { const checkboxInstances = []; - const containers = renderer.element.querySelectorAll(".checkbox"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const spec = JSON.parse(container.textContent); - const html = \`
+ for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const html = \`
\`; - container.innerHTML = html; - const element = container.querySelector('input[type="checkbox"]'); - const checkboxInstance = { id: container.id, spec, element }; - checkboxInstances.push(checkboxInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "Checkbox", index2, "parse", container); - continue; - } + container.innerHTML = html; + const element = container.querySelector('input[type="checkbox"]'); + const checkboxInstance = { id: \`\${pluginName$a}-\${index2}\`, spec, element }; + checkboxInstances.push(checkboxInstance); } const instances = checkboxInstances.map((checkboxInstance) => { const { element, spec } = checkboxInstance; const initialSignals = [{ - name: spec.name, + name: spec.variableId, value: spec.value || false, priority: 1, isData: false @@ -1117,8 +954,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy ...checkboxInstance, initialSignals, recieveBatch: async (batch) => { - if (batch[spec.name]) { - const value = batch[spec.name].value; + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; element.checked = value; } }, @@ -1126,7 +963,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy element.addEventListener("change", (e) => { const value = e.target.checked; const batch = { - [spec.name]: { + [spec.variableId]: { value, isData: false } @@ -1183,9 +1020,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return cssBlocks.join("\\n\\n"); } function categorizeCss(cssContent) { + const spec = { + atRules: {} + }; const result = { - atRules: {}, - hasFlags: false + spec, + hasFlags: false, + reasons: [] }; const completeBlockAtRules = [ // Keyframes and variants @@ -1250,16 +1091,16 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy let addCurrentRule = function() { 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]; } } }; @@ -1271,26 +1112,28 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const atRuleSignature = \`@\${node.name}\${node.prelude ? \` \${csstree.generate(node.prelude)}\` : ""}\`; if (node.name === "import") { const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + 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; } if (completeBlockAtRules.includes(node.name)) { const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, css: ruleContent }; return; } if (node.block) { - if (!result.atRules[atRuleSignature]) { - result.atRules[atRuleSignature] = { + if (!spec.atRules[atRuleSignature]) { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, rules: [] }; @@ -1298,7 +1141,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy currentAtRuleSignature = atRuleSignature; } else { const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, css: ruleContent }; @@ -1320,6 +1163,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy declaration.flag = securityCheck.flag; declaration.reason = securityCheck.reason; result.hasFlags = true; + result.reasons.push(securityCheck.reason); } currentRule.declarations.push(declaration); } else if (currentRule && (node.type === "Function" || node.type === "Url" || node.type === "String" || node.type === "Identifier")) { @@ -1332,6 +1176,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy lastDecl.flag = securityCheck.flag; lastDecl.reason = securityCheck.reason; result.hasFlags = true; + result.reasons.push(securityCheck.reason); } } } @@ -1342,17 +1187,19 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } return result; } + const pluginName$9 = "css"; + const className$8 = pluginClassName(pluginName$9); const cssPlugin = { - name: "css", + ...flaggableJsonPlugin(pluginName$9, className$8), initializePlugin: (md) => { if (typeof csstree === "undefined") { throw new Error("css-tree library is required for CSS plugin. Please include the css-tree script."); } - definePlugin(md, "css"); + definePlugin(md, pluginName$9); md.block.ruler.before("fence", "css_block", function(state, startLine, endLine) { const start = state.bMarks[startLine] + state.tShift[startLine]; const max = state.eMarks[startLine]; - if (!state.src.slice(start, max).trim().startsWith("\`\`\`css")) { + if (!state.src.slice(start, max).trim().startsWith(\`\\\`\\\`\\\`\${pluginName$9}\`)) { return false; } let nextLine = startLine; @@ -1364,7 +1211,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } state.line = nextLine + 1; const token = state.push("fence", "code", 0); - token.info = "css"; + token.info = pluginName$9; token.content = state.getLines(startLine + 1, nextLine, state.blkIndent, true); token.map = [startLine, state.line]; return true; @@ -1373,11 +1220,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy md.renderer.rules.fence = function(tokens, idx, options, env, slf) { const token = tokens[idx]; const info = token.info.trim(); - if (info === "css") { - const cssId = \`css-\${idx}\`; + if (info === pluginName$9) { const cssContent = token.content.trim(); const categorizedCss = categorizeCss(cssContent); - return sanitizedHTML("div", { id: cssId, class: "css-component" }, JSON.stringify(categorizedCss)); + return sanitizedHTML("div", { id: \`\${pluginName$9}-\${idx}\`, class: className$8 }, JSON.stringify(categorizedCss), true); } if (originalFence) { return originalFence(tokens, idx, options, env, slf); @@ -1386,40 +1232,33 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } }; }, - hydrateComponent: async (renderer, errorHandler) => { + hydrateComponent: async (renderer, errorHandler, specs) => { const cssInstances = []; - const containers = renderer.element.querySelectorAll(".css-component"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const categorizedCss = JSON.parse(container.textContent); - const comments = []; - if (categorizedCss.hasFlags) { - console.warn(\`CSS security: Security issues detected in CSS\`); - comments.push(\`\`); - } - const safeCss = reconstituteCss(categorizedCss.atRules); - if (safeCss.trim().length > 0) { - const styleElement = document.createElement("style"); - styleElement.type = "text/css"; - styleElement.id = \`idocs-css-\${container.id}\`; - styleElement.textContent = safeCss; - const target = renderer.shadowRoot || document.head; - target.appendChild(styleElement); - comments.push(\`\`); - cssInstances.push({ - id: container.id, - element: styleElement - }); - } else { - comments.push(\`\`); - } - container.innerHTML = comments.join("\\n"); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "CSS", index2, "parse", container); + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { continue; } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const categorizedCss = specReview.approvedSpec; + const comments = []; + const safeCss = reconstituteCss(categorizedCss.atRules); + if (safeCss.trim().length > 0) { + const styleElement = document.createElement("style"); + styleElement.type = "text/css"; + styleElement.id = \`idocs-css-\${index2}\`; + styleElement.textContent = safeCss; + const target = renderer.shadowRoot || document.head; + target.appendChild(styleElement); + comments.push(\`\`); + cssInstances.push({ + id: \`\${pluginName$9}-\${index2}\`, + element: styleElement + }); + } else { + comments.push(\`\`); + } + container.innerHTML = comments.join("\\n"); } const instances = cssInstances.map((cssInstance) => { return { @@ -1440,51 +1279,45 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$8 = "dropdown"; + const className$7 = pluginClassName(pluginName$8); const dropdownPlugin = { - name: "dropdown", - initializePlugin: (md) => definePlugin(md, "dropdown"), - fence: (token, idx) => { - const DropdownId = \`Dropdown-\${idx}\`; - return sanitizedHTML("div", { id: DropdownId, class: "dropdown" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$8, className$7), + hydrateComponent: async (renderer, errorHandler, specs) => { const dropdownInstances = []; - const containers = renderer.element.querySelectorAll(".dropdown"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const spec = JSON.parse(container.textContent); - const html = \`
+ for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const html = \`
\`; - container.innerHTML = html; - const element = container.querySelector("select"); - setSelectOptions(element, spec.multiple ?? false, spec.options ?? [], spec.value ?? (spec.multiple ? [] : "")); - const dropdownInstance = { id: container.id, spec, element }; - dropdownInstances.push(dropdownInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "Dropdown", index2, "parse", container); - continue; - } + container.innerHTML = html; + const element = container.querySelector("select"); + setSelectOptions(element, spec.multiple ?? false, spec.options ?? [], spec.value ?? (spec.multiple ? [] : "")); + const dropdownInstance = { id: \`\${pluginName$8}-\${index2}\`, spec, element }; + dropdownInstances.push(dropdownInstance); } const instances = dropdownInstances.map((dropdownInstance, index2) => { const { element, spec } = dropdownInstance; const initialSignals = [{ - name: spec.name, + name: spec.variableId, value: spec.value || null, priority: 1, isData: false }]; if (spec.dynamicOptions) { initialSignals.push({ - name: spec.dynamicOptions.dataSignalName, + name: spec.dynamicOptions.dataSourceName, value: null, priority: -1, isData: true @@ -1496,8 +1329,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy recieveBatch: async (batch) => { var _a, _b; const { dynamicOptions } = spec; - if (dynamicOptions == null ? void 0 : dynamicOptions.dataSignalName) { - const newData = (_a = batch[dynamicOptions.dataSignalName]) == null ? void 0 : _a.value; + if (dynamicOptions == null ? void 0 : dynamicOptions.dataSourceName) { + const newData = (_a = batch[dynamicOptions.dataSourceName]) == null ? void 0 : _a.value; if (newData) { let hasFieldName = false; const uniqueOptions = /* @__PURE__ */ new Set(); @@ -1512,7 +1345,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const existingSelection = spec.multiple ? Array.from(element.selectedOptions).map((option) => option.value) : element.value; setSelectOptions(element, spec.multiple ?? false, options, existingSelection); if (!spec.multiple) { - element.value = ((_b = batch[spec.name]) == null ? void 0 : _b.value) || options[0]; + element.value = ((_b = batch[spec.variableId]) == null ? void 0 : _b.value) || options[0]; } } else { element.innerHTML = ""; @@ -1524,8 +1357,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } } } - if (batch[spec.name]) { - const value = batch[spec.name].value; + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; if (spec.multiple) { Array.from(element.options).forEach((option) => { option.selected = !!(value && Array.isArray(value) && value.includes(option.value)); @@ -1539,7 +1372,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy element.addEventListener("change", (e) => { const value = spec.multiple ? Array.from(e.target.selectedOptions).map((option) => option.value) : e.target.value; const batch = { - [spec.name]: { + [spec.variableId]: { value, isData: false } @@ -1599,51 +1432,46 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$7 = "image"; + const className$6 = pluginClassName(pluginName$7); const imagePlugin = { - name: "image", - initializePlugin: (md) => definePlugin(md, "image"), - fence: (token, idx) => { - const ImageId = \`Image-\${idx}\`; - return sanitizedHTML("div", { id: ImageId, class: "image" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$7, className$6), + hydrateComponent: async (renderer, errorHandler, specs) => { const imageInstances = []; - const containers = renderer.element.querySelectorAll(".image"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const spec = JSON.parse(container.textContent); - const element = document.createElement("img"); - const spinner = document.createElement("div"); - spinner.innerHTML = \` + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const element = document.createElement("img"); + const spinner = document.createElement("div"); + spinner.innerHTML = \` \`; - if (spec.alt) element.alt = spec.alt; - if (spec.width) element.width = spec.width; - if (spec.height) element.height = spec.height; - element.onload = () => { - spinner.style.display = "none"; - element.style.opacity = "1"; - }; - element.onerror = () => { - spinner.style.display = "none"; - element.style.opacity = "0.5"; - errorHandler(new Error("Image failed to load"), "image", index2, "load", container, element.src); - }; - container.style.position = "relative"; - spinner.style.position = "absolute"; - container.innerHTML = ""; - container.appendChild(spinner); - container.appendChild(element); - const imageInstance = { id: container.id, spec, element, spinner }; - imageInstances.push(imageInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "Image", index2, "parse", container); - } + if (spec.alt) element.alt = spec.alt; + if (spec.width) element.width = spec.width; + if (spec.height) element.height = spec.height; + element.onload = () => { + spinner.style.display = "none"; + element.style.opacity = "1"; + }; + element.onerror = () => { + spinner.style.display = "none"; + element.style.opacity = "0.5"; + errorHandler(new Error("Image failed to load"), pluginName$7, index2, "load", container, element.src); + }; + container.style.position = "relative"; + spinner.style.position = "absolute"; + container.innerHTML = ""; + container.appendChild(spinner); + container.appendChild(element); + const imageInstance = { id: \`\${pluginName$7}-\${index2}\`, spec, element, spinner }; + imageInstances.push(imageInstance); } const instances = imageInstances.map((imageInstance, index2) => { const { element, spinner, id, spec } = imageInstance; @@ -1713,8 +1541,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } return token; } + const pluginName$6 = "placeholders"; const placeholdersPlugin = { - name: "placeholders", + name: pluginName$6, initializePlugin: async (md) => { md.use(function(md2) { md2.inline.ruler.after("emphasis", "dynamic_placeholder", function(state, silent) { @@ -1802,7 +1631,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }); const instances = [ { - id: "placeholders", + id: pluginName$6, initialSignals, recieveBatch: async (batch) => { var _a; @@ -1849,28 +1678,20 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$5 = "presets"; + const className$5 = pluginClassName(pluginName$5); const presetsPlugin = { - name: "presets", - initializePlugin: (md) => definePlugin(md, "presets"), - fence: (token, idx) => { - const spec = JSON.parse(token.content.trim()); - const pluginId = \`preset-\${idx}\`; - return sanitizedHTML("div", { id: pluginId, class: "presets" }, JSON.stringify(spec)); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$5, className$5), + hydrateComponent: async (renderer, errorHandler, specs) => { const presetsInstances = []; - const containers = renderer.element.querySelectorAll(".presets"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - const id = \`presets\${index2}\`; - let presets; - try { - presets = JSON.parse(container.textContent); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "presets", index2, "parse", container); + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { continue; } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const id = \`\${pluginName$5}-\${index2}\`; + const presets = specReview.approvedSpec; if (!Array.isArray(presets)) { container.innerHTML = '
Expected an array of presets
'; continue; @@ -1955,57 +1776,146 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$4 = "slider"; + const className$4 = pluginClassName(pluginName$4); + const sliderPlugin = { + ...flaggableJsonPlugin(pluginName$4, className$4), + hydrateComponent: async (renderer, errorHandler, specs) => { + const sliderInstances = []; + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const html = \`
+
+ +
+
\`; + container.innerHTML = html; + const element = container.querySelector('input[type="range"]'); + const sliderInstance = { id: \`\${pluginName$4}-\${index2}\`, spec, element }; + sliderInstances.push(sliderInstance); + } + const instances = sliderInstances.map((sliderInstance) => { + var _a; + const { element, spec } = sliderInstance; + const valueSpan = (_a = element.parentElement) == null ? void 0 : _a.querySelector(".vega-bind-value"); + const initialSignals = [{ + name: spec.variableId, + value: spec.value || spec.min, + priority: 1, + isData: false + }]; + return { + ...sliderInstance, + initialSignals, + recieveBatch: async (batch) => { + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; + element.value = value.toString(); + if (valueSpan) { + valueSpan.textContent = value.toString(); + } + } + }, + beginListening() { + const updateValue = (e) => { + const value = parseFloat(e.target.value); + if (valueSpan) { + valueSpan.textContent = value.toString(); + } + const batch = { + [spec.variableId]: { + value, + isData: false + } + }; + renderer.signalBus.broadcast(sliderInstance.id, batch); + }; + element.addEventListener("input", updateValue); + element.addEventListener("change", updateValue); + }, + getCurrentSignalValue: () => { + return parseFloat(element.value); + }, + destroy: () => { + element.removeEventListener("input", sliderInstance.element.oninput); + element.removeEventListener("change", sliderInstance.element.onchange); + } + }; + }); + return instances; + } + }; + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + function inspectTabulatorSpec(spec) { + const flaggableSpec = { + spec + }; + return flaggableSpec; + } + const pluginName$3 = "tabulator"; + const className$3 = pluginClassName(pluginName$3); const tabulatorPlugin = { - name: "tabulator", - initializePlugin: (md) => definePlugin(md, "tabulator"), - fence: (token, idx) => { - const tabulatorId = \`tabulator-\${idx}\`; - return sanitizedHTML("div", { id: tabulatorId, class: "tabulator", style: "box-sizing: border-box;" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$3, className$3, inspectTabulatorSpec, { style: "box-sizing: border-box;" }), + hydrateComponent: async (renderer, errorHandler, specs) => { const tabulatorInstances = []; - const containers = renderer.element.querySelectorAll(".tabulator"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - if (!Tabulator) { - errorHandler(new Error("Tabulator not found"), "tabulator", index2, "init", container); + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { continue; } - try { - const spec = JSON.parse(container.textContent); - let options = { - autoColumns: true, - layout: "fitColumns", - maxHeight: "200px" - }; - if (spec.options && Object.keys(spec.options).length > 0) { - options = spec.options; - } - const table = new Tabulator(container, options); - const tabulatorInstance = { id: container.id, spec, table, built: false }; - table.on("tableBuilt", () => { - table.off("tableBuilt"); - tabulatorInstance.built = true; - }); - tabulatorInstances.push(tabulatorInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "tabulator", index2, "parse", container); + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + if (!Tabulator && index2 === 0) { + errorHandler(new Error("Tabulator not found"), pluginName$3, index2, "init", container); + continue; + } + const spec = specReview.approvedSpec; + if (!spec.dataSourceName || !spec.variableId) { + errorHandler(new Error("Tabulator requires dataSourceName and variableId"), pluginName$3, index2, "init", container); + continue; + } else if (spec.dataSourceName === spec.variableId) { + errorHandler(new Error("Tabulator dataSourceName and variableId cannot be the same"), pluginName$3, index2, "init", container); continue; } + let options = { + autoColumns: true, + layout: "fitColumns", + maxHeight: "200px" + }; + if (spec.tabulatorOptions && Object.keys(spec.tabulatorOptions).length > 0) { + options = spec.tabulatorOptions; + } + const table = new Tabulator(container, options); + const tabulatorInstance = { id: \`\${pluginName$3}-\${index2}\`, spec, table, built: false }; + table.on("tableBuilt", () => { + table.off("tableBuilt"); + tabulatorInstance.built = true; + }); + tabulatorInstances.push(tabulatorInstance); } - const dataNameSelectedSuffix = defaultCommonOptions.dataNameSelectedSuffix; const instances = tabulatorInstances.map((tabulatorInstance, index2) => { var _a; const initialSignals = [{ - name: tabulatorInstance.spec.dataSignalName, + name: tabulatorInstance.spec.dataSourceName, value: null, priority: -1, isData: true }]; - if ((_a = tabulatorInstance.spec.options) == null ? void 0 : _a.selectableRows) { + if ((_a = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a.selectableRows) { initialSignals.push({ - name: \`\${tabulatorInstance.spec.dataSignalName}\${dataNameSelectedSuffix}\`, + name: tabulatorInstance.spec.variableId, value: [], priority: -1, isData: true @@ -2016,7 +1926,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy initialSignals, recieveBatch: async (batch) => { var _a2; - const newData = (_a2 = batch[tabulatorInstance.spec.dataSignalName]) == null ? void 0 : _a2.value; + const newData = (_a2 = batch[tabulatorInstance.spec.dataSourceName]) == null ? void 0 : _a2.value; if (newData) { if (!tabulatorInstance.built) { tabulatorInstance.table.off("tableBuilt"); @@ -2032,15 +1942,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }, beginListening(sharedSignals) { var _a2; - if ((_a2 = tabulatorInstance.spec.options) == null ? void 0 : _a2.selectableRows) { + if ((_a2 = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a2.selectableRows) { for (const { isData, signalName } of sharedSignals) { if (isData) { - const matchData = signalName === \`\${tabulatorInstance.spec.dataSignalName}\${dataNameSelectedSuffix}\`; + const matchData = signalName === tabulatorInstance.spec.variableId; if (matchData) { tabulatorInstance.table.on("rowSelectionChanged", (e, rows) => { const selectedData = tabulatorInstance.table.getSelectedData(); const batch = { - [\`\${tabulatorInstance.spec.dataSignalName}\${dataNameSelectedSuffix}\`]: { + [tabulatorInstance.spec.variableId]: { value: selectedData, isData: true } @@ -2063,70 +1973,223 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }); return instances; } - }; - /*! - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ - const vegaLitePlugin = { - name: "vega-lite", - initializePlugin: (md) => definePlugin(md, "vega-lite"), - fence: (token, idx) => { - const vegaLiteId = \`vega-lite-\${idx}\`; - return sanitizedHTML("div", { id: vegaLiteId, class: "vega-chart" }, token.content.trim()); + }; + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + const pluginName$2 = "textbox"; + const className$2 = pluginClassName(pluginName$2); + const textboxPlugin = { + ...flaggableJsonPlugin(pluginName$2, className$2), + hydrateComponent: async (renderer, errorHandler, specs) => { + const textboxInstances = []; + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const placeholderAttr = spec.placeholder ? \` placeholder="\${spec.placeholder}"\` : ""; + const inputElement = spec.multiline ? \`\` : \`\`; + const html = \`
+
+ +
+
\`; + container.innerHTML = html; + const element = container.querySelector(spec.multiline ? "textarea" : 'input[type="text"]'); + const textboxInstance = { id: \`\${pluginName$2}-\${index2}\`, spec, element }; + textboxInstances.push(textboxInstance); + } + const instances = textboxInstances.map((textboxInstance) => { + const { element, spec } = textboxInstance; + const initialSignals = [{ + name: spec.variableId, + value: spec.value || "", + priority: 1, + isData: false + }]; + return { + ...textboxInstance, + initialSignals, + recieveBatch: async (batch) => { + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; + element.value = value; + } + }, + beginListening() { + const updateValue = (e) => { + const value = e.target.value; + const batch = { + [spec.variableId]: { + value, + isData: false + } + }; + renderer.signalBus.broadcast(textboxInstance.id, batch); + }; + element.addEventListener("input", updateValue); + element.addEventListener("change", updateValue); + }, + getCurrentSignalValue: () => { + return element.value; + }, + destroy: () => { + element.removeEventListener("input", textboxInstance.element.oninput); + element.removeEventListener("change", textboxInstance.element.onchange); + } + }; + }); + return instances; + } + }; + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + var LogLevel = /* @__PURE__ */ ((LogLevel2) => { + LogLevel2[LogLevel2["none"] = 0] = "none"; + LogLevel2[LogLevel2["some"] = 1] = "some"; + LogLevel2[LogLevel2["all"] = 2] = "all"; + return LogLevel2; + })(LogLevel || {}); + class SignalBus { + constructor(dataSignalPrefix) { + __publicField(this, "broadcastingStack"); + __publicField(this, "logLevel"); + __publicField(this, "logWatchIds"); + __publicField(this, "active"); + __publicField(this, "peers"); + __publicField(this, "signalDeps"); + __publicField(this, "peerDependencies"); + this.dataSignalPrefix = dataSignalPrefix; + this.logLevel = 0; + this.logWatchIds = []; + this.reset(); + } + log(id, message, ...optionalParams) { + if (this.logLevel === 0) return; + if (this.logWatchIds.length > 0 && !this.logWatchIds.includes(id)) return; + console.log(\`[Signal Bus][\${id}] \${message}\`, ...optionalParams); + } + async broadcast(originId, batch) { + if (this.broadcastingStack.includes(originId)) { + this.log(originId, "Additional broadcast from", originId, this.broadcastingStack.join(" -> ")); + } + this.log(originId, "Broadcasting batch from", originId, batch); + this.broadcastingStack.push(originId); + for (const peerId of this.peerDependencies[originId]) { + const peer = this.peers.find((p) => p.id === peerId); + if (!peer) continue; + const peerBatch = {}; + let hasBatch = false; + for (const signalName in batch) { + if (peer.initialSignals.some((s) => s.name === signalName) && batch[signalName].value !== this.signalDeps[signalName].value) { + peerBatch[signalName] = batch[signalName]; + hasBatch = true; + } + } + if (!hasBatch) continue; + peer.recieveBatch && await peer.recieveBatch(peerBatch, originId); + } + this.broadcastingStack.pop(); + for (const signalName in batch) { + const signalDep = this.signalDeps[signalName]; + signalDep.value = batch[signalName].value; + } + if (this.broadcastingStack.length === 0) { + for (const peer of this.peers) { + peer.broadcastComplete && await peer.broadcastComplete(); + } + } + } + getPriorityPeer(signalName) { + const signalDep = this.signalDeps[signalName]; + if (!signalDep) return null; + return this.peers.find((p) => p.id === signalDep.initialPriorityId); } - }; - async function resolveSpec(textContent) { - try { - const either = JSON.parse(textContent); - if (typeof either === "object") { - return resolveToVega(either); - } else { - return { error: new Error(\`Spec must be either a JSON object or a string url, found type \${typeof either}\`) }; - } - } catch (error) { - if (textContent.startsWith("http://") || textContent.startsWith("https://") || textContent.startsWith("//")) { - try { - const response = await fetch(textContent); - const either = await response.json(); - if (typeof either === "object") { - return resolveToVega(either); - } else { - return { error: new Error(\`Expected a JSON object, found type \${typeof either}\`) }; + registerPeer(peer) { + this.log("registerPeer", "register", peer); + this.peers.push(peer); + for (const initialSignal of peer.initialSignals) { + if (!(initialSignal.name in this.signalDeps)) { + this.signalDeps[initialSignal.name] = { + deps: [peer], + priority: initialSignal.priority, + initialPriorityId: peer.id, + value: initialSignal.value, + isData: initialSignal.isData + }; + } else { + const signalDep = this.signalDeps[initialSignal.name]; + if (!signalDep.deps.includes(peer)) { + signalDep.deps.push(peer); + } + if (initialSignal.priority > signalDep.priority) { + signalDep.priority = initialSignal.priority; + signalDep.initialPriorityId = peer.id; + signalDep.value = initialSignal.value; + signalDep.isData = initialSignal.isData; } - } catch (error2) { - return { error: error2 }; } - } else { - return { error: new Error("Spec string must be a url") }; } } - } - function resolveToVega(either) { - if ("$schema" in either && typeof either.$schema === "string") { - if (either.$schema.includes("vega-lite")) { - try { - const runtime = vegaLite.compile(either); - const { spec } = runtime; - return { spec }; - } catch (error) { - return { error }; + beginListening() { + this.log("beginListening", "begin initial batch", this.signalDeps); + for (const peer of this.peers) { + const batch = {}; + for (const signalName in this.signalDeps) { + const signalDep = this.signalDeps[signalName]; + const { value, isData } = signalDep; + batch[signalName] = { value, isData }; } - } else if (either.$schema.includes("vega")) { - return { spec: either }; - } else { - return { error: new Error("$schema property must be a string with vega or vega-lite version.") }; + peer.recieveBatch && peer.recieveBatch(batch, "initial"); } - } else { - return { error: new Error("Missing $schema property, must be a string with vega or vega-lite version.") }; + this.log("beginListening", "end initial batch"); + const peerSignals = {}; + for (const signalName in this.signalDeps) { + const signalDep = this.signalDeps[signalName]; + if (signalDep.deps.length === 1) continue; + for (const peer of signalDep.deps) { + if (!(peer.id in peerSignals)) { + peerSignals[peer.id] = []; + this.peerDependencies[peer.id] = []; + } + peerSignals[peer.id].push({ signalName, isData: signalDep.isData }); + for (const otherPeer of signalDep.deps) { + if (otherPeer.id !== peer.id && !this.peerDependencies[peer.id].includes(otherPeer.id)) { + this.peerDependencies[peer.id].push(otherPeer.id); + } + } + } + } + this.log("beginListening", "======= dependencies =========", peerSignals, this.peerDependencies); + for (const peer of this.peers) { + const sharedSignals = peerSignals[peer.id]; + if (sharedSignals) { + this.log(peer.id, "Shared signals:", sharedSignals); + if (this.peerDependencies[peer.id]) { + this.log(peer.id, "Shared dependencies:", this.peerDependencies[peer.id]); + } + peer.beginListening && peer.beginListening(sharedSignals); + } else { + this.log(peer.id, "No shared signals"); + } + } + this.active = true; } - } - function urlParam(urlParamName, value) { - if (value === void 0 || value === null) return ""; - if (Array.isArray(value)) { - return value.map((vn) => \`\${urlParamName}[]=\${encodeURIComponent(vn)}\`).join("&"); - } else { - return \`\${urlParamName}=\${encodeURIComponent(value)}\`; + reset() { + this.signalDeps = {}; + this.active = false; + this.peers = []; + this.broadcastingStack = []; + this.peerDependencies = {}; } } /*! @@ -2134,23 +2197,30 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Licensed under the MIT License. */ const ignoredSignals = ["width", "height", "padding", "autosize", "background", "style", "parent", "datum", "item", "event", "cursor"]; + const pluginName$1 = "vega"; + const className$1 = pluginClassName(pluginName$1); + function inspectVegaSpec(spec) { + const flaggableSpec = { + spec + }; + return flaggableSpec; + } const vegaPlugin = { - name: "vega", - initializePlugin: (md) => definePlugin(md, "vega"), - fence: (token, idx) => { - const vegaId = \`vega-\${idx}\`; - return sanitizedHTML("div", { id: vegaId, class: "vega-chart" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$1, className$1, inspectVegaSpec), + hydrateComponent: async (renderer, errorHandler, specs) => { if (!expressionsInitialized) { vega.expressionFunction("urlParam", urlParam); expressionsInitialized = true; } const vegaInstances = []; - const containers = renderer.element.querySelectorAll(".vega-chart"); const specInits = []; - for (const [index2, container] of Array.from(containers).entries()) { - const specInit = await createSpecInit(container, index2, renderer, errorHandler); + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const specInit = createSpecInit(container, index2, specReview.approvedSpec); if (specInit) { specInits.push(specInit); } @@ -2166,10 +2236,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy for (const vegaInstance of vegaInstances) { if (!vegaInstance.spec.data) continue; for (const data of vegaInstance.spec.data) { - const dataSignal = dataSignals.find( - (signal) => signal.name === data.name || \`\${signal.name}\${defaultCommonOptions.dataNameSelectedSuffix}\` === data.name - //match a selection from Tabulator - ); + const dataSignal = dataSignals.find((signal) => signal.name === data.name); if (dataSignal) { vegaInstance.initialSignals.push({ name: data.name, @@ -2328,30 +2395,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } return hasAnyChange; } - async function createSpecInit(container, index2, renderer, errorHandler) { + function createSpecInit(container, index2, spec) { var _a; - if (!container.textContent) { - container.innerHTML = '
Expected a spec object or a url
'; - return; - } - let result; - try { - result = await resolveSpec(container.textContent); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "vega", index2, "resolve", container); - return; - } - if (result.error) { - container.innerHTML = \`
\${result.error.toString()}
\`; - errorHandler(result.error, "vega", index2, "resolve", container); - return; - } - if (!result.spec) { - container.innerHTML = '
Expected a spec object
'; - return; - } - const { spec } = result; const initialSignals = ((_a = spec.signals) == null ? void 0 : _a.map((signal) => { if (ignoredSignals.includes(signal.name)) return; let isData = isSignalDataBridge(signal); @@ -2370,14 +2415,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } async function createVegaInstance(specInit, renderer, errorHandler) { const { container, index: index2, initialSignals, spec } = specInit; - const id = \`vega-\${index2}\`; + const id = \`\${pluginName$1}-\${index2}\`; let runtime; let view; try { runtime = vega.parse(spec); } catch (e) { container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "vega", index2, "parse", container); + errorHandler(e, pluginName$1, index2, "parse", container); return; } try { @@ -2385,7 +2430,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy container, renderer: renderer.options.vegaRenderer, logger: new VegaLogger((error) => { - errorHandler(error, "vega", index2, "view", container); + errorHandler(error, pluginName$1, index2, "view", container); }) }); view.run(); @@ -2399,7 +2444,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } } catch (e) { container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "vega", index2, "view", container); + errorHandler(e, pluginName$1, index2, "view", container); return; } const dataSignals = initialSignals.filter((signal) => { @@ -2473,6 +2518,46 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName = "vega-lite"; + const className = pluginClassName(pluginName); + const vegaLitePlugin = { + ...flaggableJsonPlugin(pluginName, className), + fence: (token, index2) => { + let json = token.content.trim(); + let spec; + let flaggableSpec; + try { + spec = JSON.parse(json); + } catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reasons: [\`malformed JSON\`] + }; + } + if (spec) { + try { + const vegaSpec = vegaLite.compile(spec); + flaggableSpec = inspectVegaSpec(vegaSpec.spec); + } catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reasons: [\`failed to compile vega spec\`] + }; + } + } + if (flaggableSpec) { + json = JSON.stringify(flaggableSpec); + } + return sanitizedHTML("div", { class: pluginClassName(vegaPlugin.name), id: \`\${pluginName}-\${index2}\` }, json, true); + }, + hydratesBefore: vegaPlugin.name + }; + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ function registerNativePlugins() { registerMarkdownPlugin(checkboxPlugin); registerMarkdownPlugin(cssPlugin); @@ -2480,7 +2565,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy registerMarkdownPlugin(imagePlugin); registerMarkdownPlugin(placeholdersPlugin); registerMarkdownPlugin(presetsPlugin); + registerMarkdownPlugin(sliderPlugin); registerMarkdownPlugin(tabulatorPlugin); + registerMarkdownPlugin(textboxPlugin); registerMarkdownPlugin(vegaLitePlugin); registerMarkdownPlugin(vegaPlugin); } @@ -2488,6 +2575,111 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const defaultRendererOptions = { + vegaRenderer: "canvas", + useShadowDom: false, + errorHandler: (error, pluginName2, instanceIndex, phase) => { + console.error(\`Error in plugin \${pluginName2} instance \${instanceIndex} phase \${phase}\`, error); + } + }; + class Renderer { + constructor(_element, options) { + __publicField(this, "md"); + __publicField(this, "instances"); + __publicField(this, "signalBus"); + __publicField(this, "options"); + __publicField(this, "shadowRoot"); + __publicField(this, "element"); + this.options = { ...defaultRendererOptions, ...options }; + this.signalBus = this.options.signalBus || new SignalBus(defaultCommonOptions.dataSignalPrefix); + this.instances = {}; + if (this.options.useShadowDom) { + this.shadowRoot = _element.attachShadow({ mode: "open" }); + this.element = this.shadowRoot; + } else { + this.element = _element; + } + } + ensureMd() { + if (!this.md) { + this.md = create(); + } + } + async render(markdown) { + this.reset(); + const html = this.renderHtml(markdown); + this.element.innerHTML = html; + const specs = this.hydrateSpecs(); + await this.hydrate(specs); + } + renderHtml(markdown) { + this.ensureMd(); + const parsedHTML = this.md.render(markdown); + let content = parsedHTML; + if (this.options.useShadowDom) { + content = \`
\${content}
\`; + } + return content; + } + hydrateSpecs() { + this.ensureMd(); + const specs = []; + this.signalBus.log("Renderer", "hydrate specs"); + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + if (plugin.hydrateSpecs) { + specs.push(...plugin.hydrateSpecs(this, this.options.errorHandler)); + } + } + return specs; + } + async hydrate(specs) { + this.ensureMd(); + this.signalBus.log("Renderer", "hydrate components"); + const hydrationPromises = []; + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + if (plugin.hydrateComponent) { + const specsForPlugin = specs.filter((spec) => spec.pluginName === plugin.name); + hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler, specsForPlugin).then((instances) => { + return { + pluginName: plugin.name, + instances + }; + })); + } + } + try { + const pluginHydrations = await Promise.all(hydrationPromises); + for (const hydration of pluginHydrations) { + if (hydration && hydration.instances) { + this.instances[hydration.pluginName] = hydration.instances; + for (const instance of hydration.instances) { + this.signalBus.registerPeer(instance); + } + } + } + this.signalBus.beginListening(); + } catch (error) { + console.error("Error in rendering plugins", error); + } + } + reset() { + this.signalBus.reset(); + for (const pluginName2 of Object.keys(this.instances)) { + const instances = this.instances[pluginName2]; + for (const instance of instances) { + instance.destroy && instance.destroy(); + } + } + this.instances = {}; + this.element.innerHTML = ""; + } + } + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ registerNativePlugins(); const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -2502,8 +2694,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); }); `; - const sandboxedJs = `document.addEventListener('DOMContentLoaded', () => { - const renderer = new IDocs.markdown.Renderer(document.body, { + const sandboxedJs = `let renderer; +document.addEventListener('DOMContentLoaded', () => { + let transactionIndex = 0; + const transactions = {}; + renderer = new IDocs.markdown.Renderer(document.body, { errorHandler: (error, pluginName, instanceIndex, phase, container, detail) => { console.error(\`Error in plugin \${pluginName} at instance \${instanceIndex} during \${phase}:\`, error); if (detail) { @@ -2515,12 +2710,19 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy function render(request) { if (request.markdown) { renderer.reset(); + //debugger; const html = renderer.renderHtml(request.markdown); - //todo: look at dom elements prior to hydration renderer.element.innerHTML = html; - //todo: send message to parent to ask for whitelist - //todo: asynchronously hydrate the renderer - renderer.hydrate(); + const specs = renderer.hydrateSpecs(); + const transactionId = transactionIndex++; + transactions[transactionId] = specs; + //send message to parent to ask for whitelist + const sandboxedPreRenderMessage = { + type: 'sandboxedPreHydrate', + transactionId, + specs, + }; + window.parent.postMessage(sandboxedPreRenderMessage, '*'); } } render(renderRequest); @@ -2528,15 +2730,42 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy window.addEventListener('message', (event) => { if (!event.data) return; - render(event.data); + const message = event.data; + switch (message.type) { + case 'sandboxRender': { + render(message); + break; + } + case 'sandboxApproval': { + //debugger; + //only handle if the transactionId is the latest + if (message.transactionId === transactionIndex - 1) { + //todo: console.warn of unapproved + //hydrate the renderer + const flags = transactions[message.transactionId]; + if (flags) { + renderer.hydrate(flags); + } + } + else { + console.debug('Received sandbox approval for an outdated transaction:', message.transactionId, transactionIndex); + } + break; + } + } }); }); `; class Sandbox extends Previewer { constructor(elementOrSelector, markdown, options) { super(elementOrSelector, markdown, options); + __publicField(this, "options"); __publicField(this, "iframe"); - const renderRequest = { markdown }; + this.options = options; + const renderRequest = { + type: "sandboxRender", + markdown + }; const { iframe } = createIframe(this.getDependencies(), renderRequest); this.iframe = iframe; this.element.appendChild(this.iframe); @@ -2549,6 +2778,21 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy console.error("Error loading iframe:", error); (_a = options == null ? void 0 : options.onError) == null ? void 0 : _a.call(options, new Error("Failed to load iframe")); }); + window.addEventListener("message", (event) => { + var _a; + if (event.source === this.iframe.contentWindow) { + const message = event.data; + if (message.type == "sandboxedPreHydrate") { + const specs = this.options.onApprove(message); + const sandboxedApprovalMessage = { + type: "sandboxApproval", + transactionId: message.transactionId, + specs + }; + (_a = this.iframe.contentWindow) == null ? void 0 : _a.postMessage(sandboxedApprovalMessage, "*"); + } + } + }); } destroy() { var _a; @@ -2560,7 +2804,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } send(markdown) { var _a; - (_a = this.iframe.contentWindow) == null ? void 0 : _a.postMessage({ markdown }, "*"); + const message = { + type: "sandboxRender", + markdown + }; + (_a = this.iframe.contentWindow) == null ? void 0 : _a.postMessage(message, "*"); } getDependencies() { return ` @@ -2888,12 +3136,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy ); return; } - const data = event.data; - if (data.markdown) { - host.render(data.markdown, void 0); - } else if (data.interactiveDocument) { - host.render(void 0, data.interactiveDocument); - } else { + const message = event.data; + if (message.type == "hostRenderRequest") { + if (message.markdown) { + host.render(message.markdown, void 0); + } else if (message.interactiveDocument) { + host.render(void 0, message.interactiveDocument); + } else { + } } } catch (error) { host.errorHandler( @@ -2905,11 +3155,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } function postStatus(target, message) { if (target) { - const messageWithTimestamp = { - ...message, - timestamp: Date.now() - }; - target.postMessage(messageWithTimestamp, "*"); + target.postMessage(message, "*"); } } function getElement(elementOrSelector) { @@ -2943,9 +3189,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy __publicField(this, "fileInput"); __publicField(this, "textarea"); __publicField(this, "sandbox"); + __publicField(this, "onApprove"); __publicField(this, "removeInteractionHandlers"); __publicField(this, "sandboxReady", false); this.options = { ...defaultOptions, ...options == null ? void 0 : options.options }; + this.onApprove = options.onApprove; this.removeInteractionHandlers = []; this.appDiv = getElement(options.app); this.loadingDiv = getElement(options.loading); @@ -2984,11 +3232,12 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy this.sandbox = new Sandbox(this.appDiv, markdown, { onReady: () => { this.sandboxReady = true; - postStatus(this.options.postMessageTarget, { status: "ready" }); + postStatus(this.options.postMessageTarget, { type: "hostStatus", hostStatus: "ready" }); }, onError: () => { this.errorHandler(new Error("Sandbox initialization failed"), "Sandbox could not be initialized"); - } + }, + onApprove: this.onApprove }); } errorHandler(error, detailsHtml) { @@ -3057,7 +3306,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy this.removeInteractionHandlers = []; } renderInteractiveDocument(content) { - postStatus(this.options.postMessageTarget, { status: "compiling", details: "Starting interactive document compilation" }); + postStatus(this.options.postMessageTarget, { type: "hostStatus", hostStatus: "compiling", details: "Starting interactive document compilation" }); const markdown = targetMarkdown(content); this.renderMarkdown(markdown); } @@ -3068,19 +3317,19 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy renderMarkdown(markdown) { this.hideLoadingAndHelp(); try { - postStatus(this.options.postMessageTarget, { status: "rendering", details: "Starting markdown rendering" }); + postStatus(this.options.postMessageTarget, { type: "hostStatus", hostStatus: "rendering", details: "Starting markdown rendering" }); if (!this.sandbox || !this.sandboxReady) { this.createSandbox(markdown); } else { this.sandbox.send(markdown); } - postStatus(this.options.postMessageTarget, { status: "rendered", details: "Markdown rendering completed successfully" }); + postStatus(this.options.postMessageTarget, { type: "hostStatus", hostStatus: "rendered", details: "Markdown rendering completed successfully" }); } catch (error) { this.errorHandler( error, "Error rendering markdown content" ); - postStatus(this.options.postMessageTarget, { status: "error", details: `Rendering failed: ${error.message}` }); + postStatus(this.options.postMessageTarget, { type: "hostStatus", hostStatus: "error", details: `Rendering failed: ${error.message}` }); } } } diff --git a/docs/dist/idocs.sandbox.umd.js b/docs/dist/idocs.sandbox.umd.js index cb94d9bf..e225188f 100644 --- a/docs/dist/idocs.sandbox.umd.js +++ b/docs/dist/idocs.sandbox.umd.js @@ -6,7 +6,6 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const defaultCommonOptions = { - dataNameSelectedSuffix: "_selected", dataSignalPrefix: "data_signal:", groupClassName: "group" }; @@ -62,7 +61,6 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); const defaultCommonOptions = { - dataNameSelectedSuffix: "_selected", dataSignalPrefix: "data_signal:", groupClassName: "group" }; @@ -371,7 +369,21 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy */ const plugins = []; function registerMarkdownPlugin(plugin) { - plugins.push(plugin); + let insertIndex = plugins.length; + let minIndex = 0; + for (let i = 0; i < plugins.length; i++) { + if (plugins[i].hydratesBefore === plugin.name) { + minIndex = Math.max(minIndex, i + 1); + } + } + if (plugin.hydratesBefore) { + const targetIndex = plugins.findIndex((p) => p.name === plugin.hydratesBefore); + if (targetIndex !== -1) { + insertIndex = targetIndex; + } + } + insertIndex = Math.max(insertIndex, minIndex); + plugins.splice(insertIndex, 0, plugin); return "register"; } function create() { @@ -387,8 +399,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const token = tokens[idx]; const info = token.info.trim(); if (info.startsWith("json ")) { - const pluginName = info.slice(5).trim(); - const plugin = plugins.find((p) => p.name === pluginName); + const pluginName2 = info.slice(5).trim(); + const plugin = plugins.find((p) => p.name === pluginName2); if (plugin && plugin.fence) { return plugin.fence(token, idx); } @@ -401,11 +413,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }; return md; } - function definePlugin(md, pluginName) { - md.block.ruler.before("fence", \`\${pluginName}_block\`, function(state, startLine, endLine) { + function definePlugin(md, pluginName2) { + md.block.ruler.before("fence", \`\${pluginName2}_block\`, function(state, startLine, endLine) { const start = state.bMarks[startLine] + state.tShift[startLine]; const max = state.eMarks[startLine]; - const marker = \`json \${pluginName}\`; + const marker = \`json \${pluginName2}\`; if (!state.src.slice(start, max).trim().startsWith("\`\`\`" + marker)) { return false; } @@ -424,291 +436,138 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return true; }); } - /*! - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ - var LogLevel = /* @__PURE__ */ ((LogLevel2) => { - LogLevel2[LogLevel2["none"] = 0] = "none"; - LogLevel2[LogLevel2["some"] = 1] = "some"; - LogLevel2[LogLevel2["all"] = 2] = "all"; - return LogLevel2; - })(LogLevel || {}); - class SignalBus { - constructor(dataSignalPrefix) { - __publicField(this, "broadcastingStack"); - __publicField(this, "logLevel"); - __publicField(this, "logWatchIds"); - __publicField(this, "active"); - __publicField(this, "peers"); - __publicField(this, "signalDeps"); - __publicField(this, "peerDependencies"); - this.dataSignalPrefix = dataSignalPrefix; - this.logLevel = 0; - this.logWatchIds = []; - this.reset(); - } - log(id, message, ...optionalParams) { - if (this.logLevel === 0) return; - if (this.logWatchIds.length > 0 && !this.logWatchIds.includes(id)) return; - console.log(\`[Signal Bus][\${id}] \${message}\`, ...optionalParams); - } - async broadcast(originId, batch) { - if (this.broadcastingStack.includes(originId)) { - this.log(originId, "Additional broadcast from", originId, this.broadcastingStack.join(" -> ")); - } - this.log(originId, "Broadcasting batch from", originId, batch); - this.broadcastingStack.push(originId); - for (const peerId of this.peerDependencies[originId]) { - const peer = this.peers.find((p) => p.id === peerId); - if (!peer) continue; - const peerBatch = {}; - let hasBatch = false; - for (const signalName in batch) { - if (peer.initialSignals.some((s) => s.name === signalName) && batch[signalName].value !== this.signalDeps[signalName].value) { - peerBatch[signalName] = batch[signalName]; - hasBatch = true; - } - } - if (!hasBatch) continue; - peer.recieveBatch && await peer.recieveBatch(peerBatch, originId); - } - this.broadcastingStack.pop(); - for (const signalName in batch) { - const signalDep = this.signalDeps[signalName]; - signalDep.value = batch[signalName].value; - } - if (this.broadcastingStack.length === 0) { - for (const peer of this.peers) { - peer.broadcastComplete && await peer.broadcastComplete(); - } - } - } - getPriorityPeer(signalName) { - const signalDep = this.signalDeps[signalName]; - if (!signalDep) return null; - return this.peers.find((p) => p.id === signalDep.initialPriorityId); - } - registerPeer(peer) { - this.peers.push(peer); - for (const initialSignal of peer.initialSignals) { - if (!(initialSignal.name in this.signalDeps)) { - this.signalDeps[initialSignal.name] = { - deps: [peer], - priority: initialSignal.priority, - initialPriorityId: peer.id, - value: initialSignal.value, - isData: initialSignal.isData - }; - } else { - const signalDep = this.signalDeps[initialSignal.name]; - if (!signalDep.deps.includes(peer)) { - signalDep.deps.push(peer); - } - if (initialSignal.priority > signalDep.priority) { - signalDep.priority = initialSignal.priority; - signalDep.initialPriorityId = peer.id; - signalDep.value = initialSignal.value; - signalDep.isData = initialSignal.isData; - } - } - } - } - beginListening() { - this.log("beginListening", "begin initial batch", this.signalDeps); - for (const peer of this.peers) { - const batch = {}; - for (const signalName in this.signalDeps) { - const signalDep = this.signalDeps[signalName]; - const { value, isData } = signalDep; - batch[signalName] = { value, isData }; - } - peer.recieveBatch && peer.recieveBatch(batch, "initial"); - } - this.log("beginListening", "end initial batch"); - const peerSignals = {}; - for (const signalName in this.signalDeps) { - const signalDep = this.signalDeps[signalName]; - if (signalDep.deps.length === 1) continue; - for (const peer of signalDep.deps) { - if (!(peer.id in peerSignals)) { - peerSignals[peer.id] = []; - this.peerDependencies[peer.id] = []; - } - peerSignals[peer.id].push({ signalName, isData: signalDep.isData }); - for (const otherPeer of signalDep.deps) { - if (otherPeer.id !== peer.id && !this.peerDependencies[peer.id].includes(otherPeer.id)) { - this.peerDependencies[peer.id].push(otherPeer.id); - } - } - } - } - this.log("beginListening", "======= dependencies =========", peerSignals, this.peerDependencies); - for (const peer of this.peers) { - const sharedSignals = peerSignals[peer.id]; - if (sharedSignals) { - this.log(peer.id, "Shared signals:", sharedSignals); - if (this.peerDependencies[peer.id]) { - this.log(peer.id, "Shared dependencies:", this.peerDependencies[peer.id]); - } - peer.beginListening && peer.beginListening(sharedSignals); - } else { - this.log(peer.id, "No shared signals"); - } - } - this.active = true; - } - reset() { - this.signalDeps = {}; - this.active = false; - this.peers = []; - this.broadcastingStack = []; - this.peerDependencies = {}; + function urlParam(urlParamName, value) { + if (value === void 0 || value === null) return ""; + if (Array.isArray(value)) { + return value.map((vn) => \`\${urlParamName}[]=\${encodeURIComponent(vn)}\`).join("&"); + } else { + return \`\${urlParamName}=\${encodeURIComponent(value)}\`; } } - /*! - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ - const defaultRendererOptions = { - vegaRenderer: "canvas", - useShadowDom: false, - errorHandler: (error, pluginName, instanceIndex, phase) => { - console.error(\`Error in plugin \${pluginName} instance \${instanceIndex} phase \${phase}\`, error); - } - }; - class Renderer { - constructor(_element, options) { - __publicField(this, "md"); - __publicField(this, "instances"); - __publicField(this, "signalBus"); - __publicField(this, "options"); - __publicField(this, "shadowRoot"); - __publicField(this, "element"); - this.options = { ...defaultRendererOptions, ...options }; - this.signalBus = this.options.signalBus || new SignalBus(defaultCommonOptions.dataSignalPrefix); - this.instances = {}; - if (this.options.useShadowDom) { - this.shadowRoot = _element.attachShadow({ mode: "open" }); - this.element = this.shadowRoot; - } else { - this.element = _element; - } - } - ensureMd() { - if (!this.md) { - this.md = create(); - } - } - async render(markdown) { - await this.reset(); - const content = this.renderHtml(markdown); - this.element.innerHTML = content; - await this.hydrate(); - } - renderHtml(markdown) { - this.ensureMd(); - const parsedHTML = this.md.render(markdown); - let content = parsedHTML; - if (this.options.useShadowDom) { - content = \`
\${content}
\`; - } - return content; + function getJsonScriptTag(container, errorHandler) { + const scriptTag = container.previousElementSibling; + if ((scriptTag == null ? void 0 : scriptTag.tagName) !== "SCRIPT" || scriptTag.getAttribute("type") !== "application/json") { + errorHandler(new Error("Invalid JSON script tag")); + return null; } - async hydrate() { - this.ensureMd(); - this.signalBus.log("Renderer", "rendering DOM"); - const hydrationPromises = []; - for (let i = 0; i < plugins.length; i++) { - const plugin = plugins[i]; - if (plugin.hydrateComponent) { - hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler).then((instances) => { - return { - pluginName: plugin.name, - instances - }; - })); - } - } - try { - const pluginHydrations = await Promise.all(hydrationPromises); - for (const hydration of pluginHydrations) { - if (hydration && hydration.instances) { - this.instances[hydration.pluginName] = hydration.instances; - for (const instance of hydration.instances) { - this.signalBus.registerPeer(instance); - } - } - } - this.signalBus.beginListening(); - } catch (error) { - console.error("Error in rendering plugins", error); - } + if (!scriptTag.textContent) { + errorHandler(new Error("Empty JSON script tag")); + return null; } - reset() { - this.signalBus.reset(); - for (const pluginName of Object.keys(this.instances)) { - const instances = this.instances[pluginName]; - for (const instance of instances) { - instance.destroy && instance.destroy(); - } - } - this.instances = {}; - this.element.innerHTML = ""; + try { + return JSON.parse(scriptTag.textContent); + } catch (error) { + errorHandler(error); + return null; } } + function pluginClassName(pluginName2) { + return \`chartifact-plugin-\${pluginName2}\`; + } /*! * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ - function sanitizedHTML(tagName, attributes, content) { + function sanitizedHTML(tagName, attributes, content, precedeWithScriptTag) { const element = document.createElement(tagName); Object.keys(attributes).forEach((key) => { element.setAttribute(key, attributes[key]); }); - element.textContent = content; + if (precedeWithScriptTag) { + const scriptElement = document.createElement("script"); + scriptElement.setAttribute("type", "application/json"); + const safeContent = content.replace(/<\\/script>/gi, "<\\\\/script>"); + scriptElement.innerHTML = safeContent; + return scriptElement.outerHTML + element.outerHTML; + } else { + element.textContent = content; + } return element.outerHTML; } + function flaggableJsonPlugin(pluginName2, className2, flagger, attrs) { + const plugin = { + name: pluginName2, + initializePlugin: (md) => definePlugin(md, pluginName2), + fence: (token, index2) => { + let json = token.content.trim(); + let spec; + let flaggableSpec; + try { + spec = JSON.parse(json); + } catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reasons: [\`malformed JSON\`] + }; + } + if (spec) { + if (flagger) { + flaggableSpec = flagger(spec); + } else { + flaggableSpec = { spec }; + } + } + if (flaggableSpec) { + json = JSON.stringify(flaggableSpec); + } + return sanitizedHTML("div", { class: className2, id: \`\${pluginName2}-\${index2}\`, ...attrs }, json, true); + }, + hydrateSpecs: (renderer, errorHandler) => { + var _a; + const flagged = []; + const containers = renderer.element.querySelectorAll(\`.\${className2}\`); + for (const [index2, container] of Array.from(containers).entries()) { + const flaggableSpec = getJsonScriptTag(container, (e) => errorHandler(e, pluginName2, index2, "parse", container)); + if (!flaggableSpec) continue; + const f = { approvedSpec: null, pluginName: pluginName2, containerId: container.id }; + if (flaggableSpec.hasFlags) { + f.blockedSpec = flaggableSpec.spec; + f.reason = ((_a = flaggableSpec.reasons) == null ? void 0 : _a.join(", ")) || "Unknown reason"; + } else { + f.approvedSpec = flaggableSpec.spec; + } + flagged.push(f); + } + return flagged; + } + }; + return plugin; + } /*! * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$a = "checkbox"; + const className$9 = pluginClassName(pluginName$a); const checkboxPlugin = { - name: "checkbox", - initializePlugin: (md) => definePlugin(md, "checkbox"), - fence: (token, idx) => { - const CheckboxId = \`Checkbox-\${idx}\`; - return sanitizedHTML("div", { id: CheckboxId, class: "checkbox" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$a, className$9), + hydrateComponent: async (renderer, errorHandler, specs) => { const checkboxInstances = []; - const containers = renderer.element.querySelectorAll(".checkbox"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const spec = JSON.parse(container.textContent); - const html = \`
+ for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const html = \`
\`; - container.innerHTML = html; - const element = container.querySelector('input[type="checkbox"]'); - const checkboxInstance = { id: container.id, spec, element }; - checkboxInstances.push(checkboxInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "Checkbox", index2, "parse", container); - continue; - } + container.innerHTML = html; + const element = container.querySelector('input[type="checkbox"]'); + const checkboxInstance = { id: \`\${pluginName$a}-\${index2}\`, spec, element }; + checkboxInstances.push(checkboxInstance); } const instances = checkboxInstances.map((checkboxInstance) => { const { element, spec } = checkboxInstance; const initialSignals = [{ - name: spec.name, + name: spec.variableId, value: spec.value || false, priority: 1, isData: false @@ -717,8 +576,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy ...checkboxInstance, initialSignals, recieveBatch: async (batch) => { - if (batch[spec.name]) { - const value = batch[spec.name].value; + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; element.checked = value; } }, @@ -726,7 +585,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy element.addEventListener("change", (e) => { const value = e.target.checked; const batch = { - [spec.name]: { + [spec.variableId]: { value, isData: false } @@ -783,9 +642,13 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return cssBlocks.join("\\n\\n"); } function categorizeCss(cssContent) { + const spec = { + atRules: {} + }; const result = { - atRules: {}, - hasFlags: false + spec, + hasFlags: false, + reasons: [] }; const completeBlockAtRules = [ // Keyframes and variants @@ -850,16 +713,16 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy let addCurrentRule = function() { 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]; } } }; @@ -871,26 +734,28 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const atRuleSignature = \`@\${node.name}\${node.prelude ? \` \${csstree.generate(node.prelude)}\` : ""}\`; if (node.name === "import") { const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + 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; } if (completeBlockAtRules.includes(node.name)) { const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, css: ruleContent }; return; } if (node.block) { - if (!result.atRules[atRuleSignature]) { - result.atRules[atRuleSignature] = { + if (!spec.atRules[atRuleSignature]) { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, rules: [] }; @@ -898,7 +763,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy currentAtRuleSignature = atRuleSignature; } else { const ruleContent = csstree.generate(node); - result.atRules[atRuleSignature] = { + spec.atRules[atRuleSignature] = { signature: atRuleSignature, css: ruleContent }; @@ -920,6 +785,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy declaration.flag = securityCheck.flag; declaration.reason = securityCheck.reason; result.hasFlags = true; + result.reasons.push(securityCheck.reason); } currentRule.declarations.push(declaration); } else if (currentRule && (node.type === "Function" || node.type === "Url" || node.type === "String" || node.type === "Identifier")) { @@ -932,6 +798,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy lastDecl.flag = securityCheck.flag; lastDecl.reason = securityCheck.reason; result.hasFlags = true; + result.reasons.push(securityCheck.reason); } } } @@ -942,17 +809,19 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } return result; } + const pluginName$9 = "css"; + const className$8 = pluginClassName(pluginName$9); const cssPlugin = { - name: "css", + ...flaggableJsonPlugin(pluginName$9, className$8), initializePlugin: (md) => { if (typeof csstree === "undefined") { throw new Error("css-tree library is required for CSS plugin. Please include the css-tree script."); } - definePlugin(md, "css"); + definePlugin(md, pluginName$9); md.block.ruler.before("fence", "css_block", function(state, startLine, endLine) { const start = state.bMarks[startLine] + state.tShift[startLine]; const max = state.eMarks[startLine]; - if (!state.src.slice(start, max).trim().startsWith("\`\`\`css")) { + if (!state.src.slice(start, max).trim().startsWith(\`\\\`\\\`\\\`\${pluginName$9}\`)) { return false; } let nextLine = startLine; @@ -964,7 +833,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } state.line = nextLine + 1; const token = state.push("fence", "code", 0); - token.info = "css"; + token.info = pluginName$9; token.content = state.getLines(startLine + 1, nextLine, state.blkIndent, true); token.map = [startLine, state.line]; return true; @@ -973,11 +842,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy md.renderer.rules.fence = function(tokens, idx, options, env, slf) { const token = tokens[idx]; const info = token.info.trim(); - if (info === "css") { - const cssId = \`css-\${idx}\`; + if (info === pluginName$9) { const cssContent = token.content.trim(); const categorizedCss = categorizeCss(cssContent); - return sanitizedHTML("div", { id: cssId, class: "css-component" }, JSON.stringify(categorizedCss)); + return sanitizedHTML("div", { id: \`\${pluginName$9}-\${idx}\`, class: className$8 }, JSON.stringify(categorizedCss), true); } if (originalFence) { return originalFence(tokens, idx, options, env, slf); @@ -986,40 +854,33 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } }; }, - hydrateComponent: async (renderer, errorHandler) => { + hydrateComponent: async (renderer, errorHandler, specs) => { const cssInstances = []; - const containers = renderer.element.querySelectorAll(".css-component"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const categorizedCss = JSON.parse(container.textContent); - const comments = []; - if (categorizedCss.hasFlags) { - console.warn(\`CSS security: Security issues detected in CSS\`); - comments.push(\`\`); - } - const safeCss = reconstituteCss(categorizedCss.atRules); - if (safeCss.trim().length > 0) { - const styleElement = document.createElement("style"); - styleElement.type = "text/css"; - styleElement.id = \`idocs-css-\${container.id}\`; - styleElement.textContent = safeCss; - const target = renderer.shadowRoot || document.head; - target.appendChild(styleElement); - comments.push(\`\`); - cssInstances.push({ - id: container.id, - element: styleElement - }); - } else { - comments.push(\`\`); - } - container.innerHTML = comments.join("\\n"); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "CSS", index2, "parse", container); + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { continue; } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const categorizedCss = specReview.approvedSpec; + const comments = []; + const safeCss = reconstituteCss(categorizedCss.atRules); + if (safeCss.trim().length > 0) { + const styleElement = document.createElement("style"); + styleElement.type = "text/css"; + styleElement.id = \`idocs-css-\${index2}\`; + styleElement.textContent = safeCss; + const target = renderer.shadowRoot || document.head; + target.appendChild(styleElement); + comments.push(\`\`); + cssInstances.push({ + id: \`\${pluginName$9}-\${index2}\`, + element: styleElement + }); + } else { + comments.push(\`\`); + } + container.innerHTML = comments.join("\\n"); } const instances = cssInstances.map((cssInstance) => { return { @@ -1040,51 +901,45 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$8 = "dropdown"; + const className$7 = pluginClassName(pluginName$8); const dropdownPlugin = { - name: "dropdown", - initializePlugin: (md) => definePlugin(md, "dropdown"), - fence: (token, idx) => { - const DropdownId = \`Dropdown-\${idx}\`; - return sanitizedHTML("div", { id: DropdownId, class: "dropdown" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$8, className$7), + hydrateComponent: async (renderer, errorHandler, specs) => { const dropdownInstances = []; - const containers = renderer.element.querySelectorAll(".dropdown"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const spec = JSON.parse(container.textContent); - const html = \`
+ for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const html = \`
\`; - container.innerHTML = html; - const element = container.querySelector("select"); - setSelectOptions(element, spec.multiple ?? false, spec.options ?? [], spec.value ?? (spec.multiple ? [] : "")); - const dropdownInstance = { id: container.id, spec, element }; - dropdownInstances.push(dropdownInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "Dropdown", index2, "parse", container); - continue; - } + container.innerHTML = html; + const element = container.querySelector("select"); + setSelectOptions(element, spec.multiple ?? false, spec.options ?? [], spec.value ?? (spec.multiple ? [] : "")); + const dropdownInstance = { id: \`\${pluginName$8}-\${index2}\`, spec, element }; + dropdownInstances.push(dropdownInstance); } const instances = dropdownInstances.map((dropdownInstance, index2) => { const { element, spec } = dropdownInstance; const initialSignals = [{ - name: spec.name, + name: spec.variableId, value: spec.value || null, priority: 1, isData: false }]; if (spec.dynamicOptions) { initialSignals.push({ - name: spec.dynamicOptions.dataSignalName, + name: spec.dynamicOptions.dataSourceName, value: null, priority: -1, isData: true @@ -1096,8 +951,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy recieveBatch: async (batch) => { var _a, _b; const { dynamicOptions } = spec; - if (dynamicOptions == null ? void 0 : dynamicOptions.dataSignalName) { - const newData = (_a = batch[dynamicOptions.dataSignalName]) == null ? void 0 : _a.value; + if (dynamicOptions == null ? void 0 : dynamicOptions.dataSourceName) { + const newData = (_a = batch[dynamicOptions.dataSourceName]) == null ? void 0 : _a.value; if (newData) { let hasFieldName = false; const uniqueOptions = /* @__PURE__ */ new Set(); @@ -1112,7 +967,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy const existingSelection = spec.multiple ? Array.from(element.selectedOptions).map((option) => option.value) : element.value; setSelectOptions(element, spec.multiple ?? false, options, existingSelection); if (!spec.multiple) { - element.value = ((_b = batch[spec.name]) == null ? void 0 : _b.value) || options[0]; + element.value = ((_b = batch[spec.variableId]) == null ? void 0 : _b.value) || options[0]; } } else { element.innerHTML = ""; @@ -1124,8 +979,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } } } - if (batch[spec.name]) { - const value = batch[spec.name].value; + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; if (spec.multiple) { Array.from(element.options).forEach((option) => { option.selected = !!(value && Array.isArray(value) && value.includes(option.value)); @@ -1139,7 +994,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy element.addEventListener("change", (e) => { const value = spec.multiple ? Array.from(e.target.selectedOptions).map((option) => option.value) : e.target.value; const batch = { - [spec.name]: { + [spec.variableId]: { value, isData: false } @@ -1199,51 +1054,46 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$7 = "image"; + const className$6 = pluginClassName(pluginName$7); const imagePlugin = { - name: "image", - initializePlugin: (md) => definePlugin(md, "image"), - fence: (token, idx) => { - const ImageId = \`Image-\${idx}\`; - return sanitizedHTML("div", { id: ImageId, class: "image" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$7, className$6), + hydrateComponent: async (renderer, errorHandler, specs) => { const imageInstances = []; - const containers = renderer.element.querySelectorAll(".image"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - try { - const spec = JSON.parse(container.textContent); - const element = document.createElement("img"); - const spinner = document.createElement("div"); - spinner.innerHTML = \` + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const element = document.createElement("img"); + const spinner = document.createElement("div"); + spinner.innerHTML = \` \`; - if (spec.alt) element.alt = spec.alt; - if (spec.width) element.width = spec.width; - if (spec.height) element.height = spec.height; - element.onload = () => { - spinner.style.display = "none"; - element.style.opacity = "1"; - }; - element.onerror = () => { - spinner.style.display = "none"; - element.style.opacity = "0.5"; - errorHandler(new Error("Image failed to load"), "image", index2, "load", container, element.src); - }; - container.style.position = "relative"; - spinner.style.position = "absolute"; - container.innerHTML = ""; - container.appendChild(spinner); - container.appendChild(element); - const imageInstance = { id: container.id, spec, element, spinner }; - imageInstances.push(imageInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "Image", index2, "parse", container); - } + if (spec.alt) element.alt = spec.alt; + if (spec.width) element.width = spec.width; + if (spec.height) element.height = spec.height; + element.onload = () => { + spinner.style.display = "none"; + element.style.opacity = "1"; + }; + element.onerror = () => { + spinner.style.display = "none"; + element.style.opacity = "0.5"; + errorHandler(new Error("Image failed to load"), pluginName$7, index2, "load", container, element.src); + }; + container.style.position = "relative"; + spinner.style.position = "absolute"; + container.innerHTML = ""; + container.appendChild(spinner); + container.appendChild(element); + const imageInstance = { id: \`\${pluginName$7}-\${index2}\`, spec, element, spinner }; + imageInstances.push(imageInstance); } const instances = imageInstances.map((imageInstance, index2) => { const { element, spinner, id, spec } = imageInstance; @@ -1313,8 +1163,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } return token; } + const pluginName$6 = "placeholders"; const placeholdersPlugin = { - name: "placeholders", + name: pluginName$6, initializePlugin: async (md) => { md.use(function(md2) { md2.inline.ruler.after("emphasis", "dynamic_placeholder", function(state, silent) { @@ -1402,7 +1253,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }); const instances = [ { - id: "placeholders", + id: pluginName$6, initialSignals, recieveBatch: async (batch) => { var _a; @@ -1449,28 +1300,20 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$5 = "presets"; + const className$5 = pluginClassName(pluginName$5); const presetsPlugin = { - name: "presets", - initializePlugin: (md) => definePlugin(md, "presets"), - fence: (token, idx) => { - const spec = JSON.parse(token.content.trim()); - const pluginId = \`preset-\${idx}\`; - return sanitizedHTML("div", { id: pluginId, class: "presets" }, JSON.stringify(spec)); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$5, className$5), + hydrateComponent: async (renderer, errorHandler, specs) => { const presetsInstances = []; - const containers = renderer.element.querySelectorAll(".presets"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - const id = \`presets\${index2}\`; - let presets; - try { - presets = JSON.parse(container.textContent); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "presets", index2, "parse", container); + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { continue; } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const id = \`\${pluginName$5}-\${index2}\`; + const presets = specReview.approvedSpec; if (!Array.isArray(presets)) { container.innerHTML = '
Expected an array of presets
'; continue; @@ -1555,57 +1398,146 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName$4 = "slider"; + const className$4 = pluginClassName(pluginName$4); + const sliderPlugin = { + ...flaggableJsonPlugin(pluginName$4, className$4), + hydrateComponent: async (renderer, errorHandler, specs) => { + const sliderInstances = []; + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const html = \`
+
+ +
+
\`; + container.innerHTML = html; + const element = container.querySelector('input[type="range"]'); + const sliderInstance = { id: \`\${pluginName$4}-\${index2}\`, spec, element }; + sliderInstances.push(sliderInstance); + } + const instances = sliderInstances.map((sliderInstance) => { + var _a; + const { element, spec } = sliderInstance; + const valueSpan = (_a = element.parentElement) == null ? void 0 : _a.querySelector(".vega-bind-value"); + const initialSignals = [{ + name: spec.variableId, + value: spec.value || spec.min, + priority: 1, + isData: false + }]; + return { + ...sliderInstance, + initialSignals, + recieveBatch: async (batch) => { + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; + element.value = value.toString(); + if (valueSpan) { + valueSpan.textContent = value.toString(); + } + } + }, + beginListening() { + const updateValue = (e) => { + const value = parseFloat(e.target.value); + if (valueSpan) { + valueSpan.textContent = value.toString(); + } + const batch = { + [spec.variableId]: { + value, + isData: false + } + }; + renderer.signalBus.broadcast(sliderInstance.id, batch); + }; + element.addEventListener("input", updateValue); + element.addEventListener("change", updateValue); + }, + getCurrentSignalValue: () => { + return parseFloat(element.value); + }, + destroy: () => { + element.removeEventListener("input", sliderInstance.element.oninput); + element.removeEventListener("change", sliderInstance.element.onchange); + } + }; + }); + return instances; + } + }; + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + function inspectTabulatorSpec(spec) { + const flaggableSpec = { + spec + }; + return flaggableSpec; + } + const pluginName$3 = "tabulator"; + const className$3 = pluginClassName(pluginName$3); const tabulatorPlugin = { - name: "tabulator", - initializePlugin: (md) => definePlugin(md, "tabulator"), - fence: (token, idx) => { - const tabulatorId = \`tabulator-\${idx}\`; - return sanitizedHTML("div", { id: tabulatorId, class: "tabulator", style: "box-sizing: border-box;" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$3, className$3, inspectTabulatorSpec, { style: "box-sizing: border-box;" }), + hydrateComponent: async (renderer, errorHandler, specs) => { const tabulatorInstances = []; - const containers = renderer.element.querySelectorAll(".tabulator"); - for (const [index2, container] of Array.from(containers).entries()) { - if (!container.textContent) continue; - if (!Tabulator) { - errorHandler(new Error("Tabulator not found"), "tabulator", index2, "init", container); + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { continue; } - try { - const spec = JSON.parse(container.textContent); - let options = { - autoColumns: true, - layout: "fitColumns", - maxHeight: "200px" - }; - if (spec.options && Object.keys(spec.options).length > 0) { - options = spec.options; - } - const table = new Tabulator(container, options); - const tabulatorInstance = { id: container.id, spec, table, built: false }; - table.on("tableBuilt", () => { - table.off("tableBuilt"); - tabulatorInstance.built = true; - }); - tabulatorInstances.push(tabulatorInstance); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "tabulator", index2, "parse", container); + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + if (!Tabulator && index2 === 0) { + errorHandler(new Error("Tabulator not found"), pluginName$3, index2, "init", container); + continue; + } + const spec = specReview.approvedSpec; + if (!spec.dataSourceName || !spec.variableId) { + errorHandler(new Error("Tabulator requires dataSourceName and variableId"), pluginName$3, index2, "init", container); + continue; + } else if (spec.dataSourceName === spec.variableId) { + errorHandler(new Error("Tabulator dataSourceName and variableId cannot be the same"), pluginName$3, index2, "init", container); continue; } + let options = { + autoColumns: true, + layout: "fitColumns", + maxHeight: "200px" + }; + if (spec.tabulatorOptions && Object.keys(spec.tabulatorOptions).length > 0) { + options = spec.tabulatorOptions; + } + const table = new Tabulator(container, options); + const tabulatorInstance = { id: \`\${pluginName$3}-\${index2}\`, spec, table, built: false }; + table.on("tableBuilt", () => { + table.off("tableBuilt"); + tabulatorInstance.built = true; + }); + tabulatorInstances.push(tabulatorInstance); } - const dataNameSelectedSuffix = defaultCommonOptions.dataNameSelectedSuffix; const instances = tabulatorInstances.map((tabulatorInstance, index2) => { var _a; const initialSignals = [{ - name: tabulatorInstance.spec.dataSignalName, + name: tabulatorInstance.spec.dataSourceName, value: null, priority: -1, isData: true }]; - if ((_a = tabulatorInstance.spec.options) == null ? void 0 : _a.selectableRows) { + if ((_a = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a.selectableRows) { initialSignals.push({ - name: \`\${tabulatorInstance.spec.dataSignalName}\${dataNameSelectedSuffix}\`, + name: tabulatorInstance.spec.variableId, value: [], priority: -1, isData: true @@ -1616,7 +1548,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy initialSignals, recieveBatch: async (batch) => { var _a2; - const newData = (_a2 = batch[tabulatorInstance.spec.dataSignalName]) == null ? void 0 : _a2.value; + const newData = (_a2 = batch[tabulatorInstance.spec.dataSourceName]) == null ? void 0 : _a2.value; if (newData) { if (!tabulatorInstance.built) { tabulatorInstance.table.off("tableBuilt"); @@ -1632,15 +1564,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }, beginListening(sharedSignals) { var _a2; - if ((_a2 = tabulatorInstance.spec.options) == null ? void 0 : _a2.selectableRows) { + if ((_a2 = tabulatorInstance.spec.tabulatorOptions) == null ? void 0 : _a2.selectableRows) { for (const { isData, signalName } of sharedSignals) { if (isData) { - const matchData = signalName === \`\${tabulatorInstance.spec.dataSignalName}\${dataNameSelectedSuffix}\`; + const matchData = signalName === tabulatorInstance.spec.variableId; if (matchData) { tabulatorInstance.table.on("rowSelectionChanged", (e, rows) => { const selectedData = tabulatorInstance.table.getSelectedData(); const batch = { - [\`\${tabulatorInstance.spec.dataSignalName}\${dataNameSelectedSuffix}\`]: { + [tabulatorInstance.spec.variableId]: { value: selectedData, isData: true } @@ -1659,74 +1591,227 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy destroy: () => { tabulatorInstance.table.destroy(); } - }; - }); - return instances; + }; + }); + return instances; + } + }; + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + const pluginName$2 = "textbox"; + const className$2 = pluginClassName(pluginName$2); + const textboxPlugin = { + ...flaggableJsonPlugin(pluginName$2, className$2), + hydrateComponent: async (renderer, errorHandler, specs) => { + const textboxInstances = []; + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const spec = specReview.approvedSpec; + const placeholderAttr = spec.placeholder ? \` placeholder="\${spec.placeholder}"\` : ""; + const inputElement = spec.multiline ? \`\` : \`\`; + const html = \`
+
+ +
+
\`; + container.innerHTML = html; + const element = container.querySelector(spec.multiline ? "textarea" : 'input[type="text"]'); + const textboxInstance = { id: \`\${pluginName$2}-\${index2}\`, spec, element }; + textboxInstances.push(textboxInstance); + } + const instances = textboxInstances.map((textboxInstance) => { + const { element, spec } = textboxInstance; + const initialSignals = [{ + name: spec.variableId, + value: spec.value || "", + priority: 1, + isData: false + }]; + return { + ...textboxInstance, + initialSignals, + recieveBatch: async (batch) => { + if (batch[spec.variableId]) { + const value = batch[spec.variableId].value; + element.value = value; + } + }, + beginListening() { + const updateValue = (e) => { + const value = e.target.value; + const batch = { + [spec.variableId]: { + value, + isData: false + } + }; + renderer.signalBus.broadcast(textboxInstance.id, batch); + }; + element.addEventListener("input", updateValue); + element.addEventListener("change", updateValue); + }, + getCurrentSignalValue: () => { + return element.value; + }, + destroy: () => { + element.removeEventListener("input", textboxInstance.element.oninput); + element.removeEventListener("change", textboxInstance.element.onchange); + } + }; + }); + return instances; + } + }; + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ + var LogLevel = /* @__PURE__ */ ((LogLevel2) => { + LogLevel2[LogLevel2["none"] = 0] = "none"; + LogLevel2[LogLevel2["some"] = 1] = "some"; + LogLevel2[LogLevel2["all"] = 2] = "all"; + return LogLevel2; + })(LogLevel || {}); + class SignalBus { + constructor(dataSignalPrefix) { + __publicField(this, "broadcastingStack"); + __publicField(this, "logLevel"); + __publicField(this, "logWatchIds"); + __publicField(this, "active"); + __publicField(this, "peers"); + __publicField(this, "signalDeps"); + __publicField(this, "peerDependencies"); + this.dataSignalPrefix = dataSignalPrefix; + this.logLevel = 0; + this.logWatchIds = []; + this.reset(); + } + log(id, message, ...optionalParams) { + if (this.logLevel === 0) return; + if (this.logWatchIds.length > 0 && !this.logWatchIds.includes(id)) return; + console.log(\`[Signal Bus][\${id}] \${message}\`, ...optionalParams); + } + async broadcast(originId, batch) { + if (this.broadcastingStack.includes(originId)) { + this.log(originId, "Additional broadcast from", originId, this.broadcastingStack.join(" -> ")); + } + this.log(originId, "Broadcasting batch from", originId, batch); + this.broadcastingStack.push(originId); + for (const peerId of this.peerDependencies[originId]) { + const peer = this.peers.find((p) => p.id === peerId); + if (!peer) continue; + const peerBatch = {}; + let hasBatch = false; + for (const signalName in batch) { + if (peer.initialSignals.some((s) => s.name === signalName) && batch[signalName].value !== this.signalDeps[signalName].value) { + peerBatch[signalName] = batch[signalName]; + hasBatch = true; + } + } + if (!hasBatch) continue; + peer.recieveBatch && await peer.recieveBatch(peerBatch, originId); + } + this.broadcastingStack.pop(); + for (const signalName in batch) { + const signalDep = this.signalDeps[signalName]; + signalDep.value = batch[signalName].value; + } + if (this.broadcastingStack.length === 0) { + for (const peer of this.peers) { + peer.broadcastComplete && await peer.broadcastComplete(); + } + } } - }; - /*! - * Copyright (c) Microsoft Corporation. - * Licensed under the MIT License. - */ - const vegaLitePlugin = { - name: "vega-lite", - initializePlugin: (md) => definePlugin(md, "vega-lite"), - fence: (token, idx) => { - const vegaLiteId = \`vega-lite-\${idx}\`; - return sanitizedHTML("div", { id: vegaLiteId, class: "vega-chart" }, token.content.trim()); + getPriorityPeer(signalName) { + const signalDep = this.signalDeps[signalName]; + if (!signalDep) return null; + return this.peers.find((p) => p.id === signalDep.initialPriorityId); } - }; - async function resolveSpec(textContent) { - try { - const either = JSON.parse(textContent); - if (typeof either === "object") { - return resolveToVega(either); - } else { - return { error: new Error(\`Spec must be either a JSON object or a string url, found type \${typeof either}\`) }; - } - } catch (error) { - if (textContent.startsWith("http://") || textContent.startsWith("https://") || textContent.startsWith("//")) { - try { - const response = await fetch(textContent); - const either = await response.json(); - if (typeof either === "object") { - return resolveToVega(either); - } else { - return { error: new Error(\`Expected a JSON object, found type \${typeof either}\`) }; + registerPeer(peer) { + this.log("registerPeer", "register", peer); + this.peers.push(peer); + for (const initialSignal of peer.initialSignals) { + if (!(initialSignal.name in this.signalDeps)) { + this.signalDeps[initialSignal.name] = { + deps: [peer], + priority: initialSignal.priority, + initialPriorityId: peer.id, + value: initialSignal.value, + isData: initialSignal.isData + }; + } else { + const signalDep = this.signalDeps[initialSignal.name]; + if (!signalDep.deps.includes(peer)) { + signalDep.deps.push(peer); + } + if (initialSignal.priority > signalDep.priority) { + signalDep.priority = initialSignal.priority; + signalDep.initialPriorityId = peer.id; + signalDep.value = initialSignal.value; + signalDep.isData = initialSignal.isData; } - } catch (error2) { - return { error: error2 }; } - } else { - return { error: new Error("Spec string must be a url") }; } } - } - function resolveToVega(either) { - if ("$schema" in either && typeof either.$schema === "string") { - if (either.$schema.includes("vega-lite")) { - try { - const runtime = vegaLite.compile(either); - const { spec } = runtime; - return { spec }; - } catch (error) { - return { error }; - } - } else if (either.$schema.includes("vega")) { - return { spec: either }; - } else { - return { error: new Error("$schema property must be a string with vega or vega-lite version.") }; + beginListening() { + this.log("beginListening", "begin initial batch", this.signalDeps); + for (const peer of this.peers) { + const batch = {}; + for (const signalName in this.signalDeps) { + const signalDep = this.signalDeps[signalName]; + const { value, isData } = signalDep; + batch[signalName] = { value, isData }; + } + peer.recieveBatch && peer.recieveBatch(batch, "initial"); } - } else { - return { error: new Error("Missing $schema property, must be a string with vega or vega-lite version.") }; + this.log("beginListening", "end initial batch"); + const peerSignals = {}; + for (const signalName in this.signalDeps) { + const signalDep = this.signalDeps[signalName]; + if (signalDep.deps.length === 1) continue; + for (const peer of signalDep.deps) { + if (!(peer.id in peerSignals)) { + peerSignals[peer.id] = []; + this.peerDependencies[peer.id] = []; + } + peerSignals[peer.id].push({ signalName, isData: signalDep.isData }); + for (const otherPeer of signalDep.deps) { + if (otherPeer.id !== peer.id && !this.peerDependencies[peer.id].includes(otherPeer.id)) { + this.peerDependencies[peer.id].push(otherPeer.id); + } + } + } + } + this.log("beginListening", "======= dependencies =========", peerSignals, this.peerDependencies); + for (const peer of this.peers) { + const sharedSignals = peerSignals[peer.id]; + if (sharedSignals) { + this.log(peer.id, "Shared signals:", sharedSignals); + if (this.peerDependencies[peer.id]) { + this.log(peer.id, "Shared dependencies:", this.peerDependencies[peer.id]); + } + peer.beginListening && peer.beginListening(sharedSignals); + } else { + this.log(peer.id, "No shared signals"); + } + } + this.active = true; } - } - function urlParam(urlParamName, value) { - if (value === void 0 || value === null) return ""; - if (Array.isArray(value)) { - return value.map((vn) => \`\${urlParamName}[]=\${encodeURIComponent(vn)}\`).join("&"); - } else { - return \`\${urlParamName}=\${encodeURIComponent(value)}\`; + reset() { + this.signalDeps = {}; + this.active = false; + this.peers = []; + this.broadcastingStack = []; + this.peerDependencies = {}; } } /*! @@ -1734,23 +1819,30 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Licensed under the MIT License. */ const ignoredSignals = ["width", "height", "padding", "autosize", "background", "style", "parent", "datum", "item", "event", "cursor"]; + const pluginName$1 = "vega"; + const className$1 = pluginClassName(pluginName$1); + function inspectVegaSpec(spec) { + const flaggableSpec = { + spec + }; + return flaggableSpec; + } const vegaPlugin = { - name: "vega", - initializePlugin: (md) => definePlugin(md, "vega"), - fence: (token, idx) => { - const vegaId = \`vega-\${idx}\`; - return sanitizedHTML("div", { id: vegaId, class: "vega-chart" }, token.content.trim()); - }, - hydrateComponent: async (renderer, errorHandler) => { + ...flaggableJsonPlugin(pluginName$1, className$1, inspectVegaSpec), + hydrateComponent: async (renderer, errorHandler, specs) => { if (!expressionsInitialized) { vega.expressionFunction("urlParam", urlParam); expressionsInitialized = true; } const vegaInstances = []; - const containers = renderer.element.querySelectorAll(".vega-chart"); const specInits = []; - for (const [index2, container] of Array.from(containers).entries()) { - const specInit = await createSpecInit(container, index2, renderer, errorHandler); + for (let index2 = 0; index2 < specs.length; index2++) { + const specReview = specs[index2]; + if (!specReview.approvedSpec) { + continue; + } + const container = renderer.element.querySelector(\`#\${specReview.containerId}\`); + const specInit = createSpecInit(container, index2, specReview.approvedSpec); if (specInit) { specInits.push(specInit); } @@ -1766,10 +1858,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy for (const vegaInstance of vegaInstances) { if (!vegaInstance.spec.data) continue; for (const data of vegaInstance.spec.data) { - const dataSignal = dataSignals.find( - (signal) => signal.name === data.name || \`\${signal.name}\${defaultCommonOptions.dataNameSelectedSuffix}\` === data.name - //match a selection from Tabulator - ); + const dataSignal = dataSignals.find((signal) => signal.name === data.name); if (dataSignal) { vegaInstance.initialSignals.push({ name: data.name, @@ -1928,30 +2017,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } return hasAnyChange; } - async function createSpecInit(container, index2, renderer, errorHandler) { + function createSpecInit(container, index2, spec) { var _a; - if (!container.textContent) { - container.innerHTML = '
Expected a spec object or a url
'; - return; - } - let result; - try { - result = await resolveSpec(container.textContent); - } catch (e) { - container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "vega", index2, "resolve", container); - return; - } - if (result.error) { - container.innerHTML = \`
\${result.error.toString()}
\`; - errorHandler(result.error, "vega", index2, "resolve", container); - return; - } - if (!result.spec) { - container.innerHTML = '
Expected a spec object
'; - return; - } - const { spec } = result; const initialSignals = ((_a = spec.signals) == null ? void 0 : _a.map((signal) => { if (ignoredSignals.includes(signal.name)) return; let isData = isSignalDataBridge(signal); @@ -1970,14 +2037,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } async function createVegaInstance(specInit, renderer, errorHandler) { const { container, index: index2, initialSignals, spec } = specInit; - const id = \`vega-\${index2}\`; + const id = \`\${pluginName$1}-\${index2}\`; let runtime; let view; try { runtime = vega.parse(spec); } catch (e) { container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "vega", index2, "parse", container); + errorHandler(e, pluginName$1, index2, "parse", container); return; } try { @@ -1985,7 +2052,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy container, renderer: renderer.options.vegaRenderer, logger: new VegaLogger((error) => { - errorHandler(error, "vega", index2, "view", container); + errorHandler(error, pluginName$1, index2, "view", container); }) }); view.run(); @@ -1999,7 +2066,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } } catch (e) { container.innerHTML = \`
\${e.toString()}
\`; - errorHandler(e, "vega", index2, "view", container); + errorHandler(e, pluginName$1, index2, "view", container); return; } const dataSignals = initialSignals.filter((signal) => { @@ -2073,6 +2140,46 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const pluginName = "vega-lite"; + const className = pluginClassName(pluginName); + const vegaLitePlugin = { + ...flaggableJsonPlugin(pluginName, className), + fence: (token, index2) => { + let json = token.content.trim(); + let spec; + let flaggableSpec; + try { + spec = JSON.parse(json); + } catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reasons: [\`malformed JSON\`] + }; + } + if (spec) { + try { + const vegaSpec = vegaLite.compile(spec); + flaggableSpec = inspectVegaSpec(vegaSpec.spec); + } catch (e) { + flaggableSpec = { + spec: null, + hasFlags: true, + reasons: [\`failed to compile vega spec\`] + }; + } + } + if (flaggableSpec) { + json = JSON.stringify(flaggableSpec); + } + return sanitizedHTML("div", { class: pluginClassName(vegaPlugin.name), id: \`\${pluginName}-\${index2}\` }, json, true); + }, + hydratesBefore: vegaPlugin.name + }; + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ function registerNativePlugins() { registerMarkdownPlugin(checkboxPlugin); registerMarkdownPlugin(cssPlugin); @@ -2080,7 +2187,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy registerMarkdownPlugin(imagePlugin); registerMarkdownPlugin(placeholdersPlugin); registerMarkdownPlugin(presetsPlugin); + registerMarkdownPlugin(sliderPlugin); registerMarkdownPlugin(tabulatorPlugin); + registerMarkdownPlugin(textboxPlugin); registerMarkdownPlugin(vegaLitePlugin); registerMarkdownPlugin(vegaPlugin); } @@ -2088,6 +2197,111 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy * Copyright (c) Microsoft Corporation. * Licensed under the MIT License. */ + const defaultRendererOptions = { + vegaRenderer: "canvas", + useShadowDom: false, + errorHandler: (error, pluginName2, instanceIndex, phase) => { + console.error(\`Error in plugin \${pluginName2} instance \${instanceIndex} phase \${phase}\`, error); + } + }; + class Renderer { + constructor(_element, options) { + __publicField(this, "md"); + __publicField(this, "instances"); + __publicField(this, "signalBus"); + __publicField(this, "options"); + __publicField(this, "shadowRoot"); + __publicField(this, "element"); + this.options = { ...defaultRendererOptions, ...options }; + this.signalBus = this.options.signalBus || new SignalBus(defaultCommonOptions.dataSignalPrefix); + this.instances = {}; + if (this.options.useShadowDom) { + this.shadowRoot = _element.attachShadow({ mode: "open" }); + this.element = this.shadowRoot; + } else { + this.element = _element; + } + } + ensureMd() { + if (!this.md) { + this.md = create(); + } + } + async render(markdown) { + this.reset(); + const html = this.renderHtml(markdown); + this.element.innerHTML = html; + const specs = this.hydrateSpecs(); + await this.hydrate(specs); + } + renderHtml(markdown) { + this.ensureMd(); + const parsedHTML = this.md.render(markdown); + let content = parsedHTML; + if (this.options.useShadowDom) { + content = \`
\${content}
\`; + } + return content; + } + hydrateSpecs() { + this.ensureMd(); + const specs = []; + this.signalBus.log("Renderer", "hydrate specs"); + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + if (plugin.hydrateSpecs) { + specs.push(...plugin.hydrateSpecs(this, this.options.errorHandler)); + } + } + return specs; + } + async hydrate(specs) { + this.ensureMd(); + this.signalBus.log("Renderer", "hydrate components"); + const hydrationPromises = []; + for (let i = 0; i < plugins.length; i++) { + const plugin = plugins[i]; + if (plugin.hydrateComponent) { + const specsForPlugin = specs.filter((spec) => spec.pluginName === plugin.name); + hydrationPromises.push(plugin.hydrateComponent(this, this.options.errorHandler, specsForPlugin).then((instances) => { + return { + pluginName: plugin.name, + instances + }; + })); + } + } + try { + const pluginHydrations = await Promise.all(hydrationPromises); + for (const hydration of pluginHydrations) { + if (hydration && hydration.instances) { + this.instances[hydration.pluginName] = hydration.instances; + for (const instance of hydration.instances) { + this.signalBus.registerPeer(instance); + } + } + } + this.signalBus.beginListening(); + } catch (error) { + console.error("Error in rendering plugins", error); + } + } + reset() { + this.signalBus.reset(); + for (const pluginName2 of Object.keys(this.instances)) { + const instances = this.instances[pluginName2]; + for (const instance of instances) { + instance.destroy && instance.destroy(); + } + } + this.instances = {}; + this.element.innerHTML = ""; + } + } + /*! + * Copyright (c) Microsoft Corporation. + * Licensed under the MIT License. + */ registerNativePlugins(); const index = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, @@ -2102,8 +2316,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy Object.defineProperty(exports2, Symbol.toStringTag, { value: "Module" }); }); `; - const sandboxedJs = `document.addEventListener('DOMContentLoaded', () => { - const renderer = new IDocs.markdown.Renderer(document.body, { + const sandboxedJs = `let renderer; +document.addEventListener('DOMContentLoaded', () => { + let transactionIndex = 0; + const transactions = {}; + renderer = new IDocs.markdown.Renderer(document.body, { errorHandler: (error, pluginName, instanceIndex, phase, container, detail) => { console.error(\`Error in plugin \${pluginName} at instance \${instanceIndex} during \${phase}:\`, error); if (detail) { @@ -2115,12 +2332,19 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy function render(request) { if (request.markdown) { renderer.reset(); + //debugger; const html = renderer.renderHtml(request.markdown); - //todo: look at dom elements prior to hydration renderer.element.innerHTML = html; - //todo: send message to parent to ask for whitelist - //todo: asynchronously hydrate the renderer - renderer.hydrate(); + const specs = renderer.hydrateSpecs(); + const transactionId = transactionIndex++; + transactions[transactionId] = specs; + //send message to parent to ask for whitelist + const sandboxedPreRenderMessage = { + type: 'sandboxedPreHydrate', + transactionId, + specs, + }; + window.parent.postMessage(sandboxedPreRenderMessage, '*'); } } render(renderRequest); @@ -2128,7 +2352,29 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy window.addEventListener('message', (event) => { if (!event.data) return; - render(event.data); + const message = event.data; + switch (message.type) { + case 'sandboxRender': { + render(message); + break; + } + case 'sandboxApproval': { + //debugger; + //only handle if the transactionId is the latest + if (message.transactionId === transactionIndex - 1) { + //todo: console.warn of unapproved + //hydrate the renderer + const flags = transactions[message.transactionId]; + if (flags) { + renderer.hydrate(flags); + } + } + else { + console.debug('Received sandbox approval for an outdated transaction:', message.transactionId, transactionIndex); + } + break; + } + } }); }); `; @@ -2136,7 +2382,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy constructor(elementOrSelector, markdown, options) { super(elementOrSelector, markdown, options); __publicField(this, "iframe"); - const renderRequest = { markdown }; + this.options = options; + const renderRequest = { + type: "sandboxRender", + markdown + }; const { iframe } = createIframe(this.getDependencies(), renderRequest); this.iframe = iframe; this.element.appendChild(this.iframe); @@ -2149,6 +2399,21 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy console.error("Error loading iframe:", error); (_a = options == null ? void 0 : options.onError) == null ? void 0 : _a.call(options, new Error("Failed to load iframe")); }); + window.addEventListener("message", (event) => { + var _a; + if (event.source === this.iframe.contentWindow) { + const message = event.data; + if (message.type == "sandboxedPreHydrate") { + const specs = this.options.onApprove(message); + const sandboxedApprovalMessage = { + type: "sandboxApproval", + transactionId: message.transactionId, + specs + }; + (_a = this.iframe.contentWindow) == null ? void 0 : _a.postMessage(sandboxedApprovalMessage, "*"); + } + } + }); } destroy() { var _a; @@ -2160,7 +2425,11 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy } send(markdown) { var _a; - (_a = this.iframe.contentWindow) == null ? void 0 : _a.postMessage({ markdown }, "*"); + const message = { + type: "sandboxRender", + markdown + }; + (_a = this.iframe.contentWindow) == null ? void 0 : _a.postMessage(message, "*"); } getDependencies() { return ` From 346e498eee5962160728fb68fc18275f0c7f5ca0 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 18:09:46 -0700 Subject: [PATCH 57/61] remove empty tabulatorOptions --- docs/assets/examples/seattle-weather/5.idoc.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/assets/examples/seattle-weather/5.idoc.json b/docs/assets/examples/seattle-weather/5.idoc.json index 9777a12e..88350e26 100644 --- a/docs/assets/examples/seattle-weather/5.idoc.json +++ b/docs/assets/examples/seattle-weather/5.idoc.json @@ -26,8 +26,7 @@ { "type": "table", "dataSourceName": "seattle_weather", - "variableId": "seattle_weather_selected", - "tabulatorOptions": {} + "variableId": "seattle_weather_selected" }, "Here is a stacked bar chart of Seattle weather:\nEach bar represents the count of weather types for each month.\nThe colors distinguish between different weather conditions such as sun, fog, drizzle, rain, and snow.", { From 0cf3264ded497c731ac1923f4146165fa1f0fc7a Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 18:26:41 -0700 Subject: [PATCH 58/61] normalize spec creation --- packages/compiler/src/md.ts | 71 ++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/packages/compiler/src/md.ts b/packages/compiler/src/md.ts index 05eda725..55bd4aec 100644 --- a/packages/compiler/src/md.ts +++ b/packages/compiler/src/md.ts @@ -1,6 +1,6 @@ import { Spec as VegaSpec } from 'vega-typings'; import { TopLevelSpec as VegaLiteSpec } from "vega-lite"; -import { ChartFull, DataSource, ElementGroup, InteractiveDocument, TableElement, TextboxElement, Variable } from 'schema'; +import { ChartFull, DataSource, ElementGroup, InteractiveDocument, TableElement, Variable } from 'schema'; import { getChartType } from './util.js'; import { addDynamicDataLoaderToSpec, addStaticDataLoaderToSpec } from './loader.js'; import { Plugins } from '@microsoft/interactive-document-markdown'; @@ -38,7 +38,7 @@ export function targetMarkdown(page: InteractiveDocument) { mdSections.push(tickWrap('css', page.layout.css)); } - const tableElements = page.groups.flatMap(group => group.elements.filter(e => typeof e !== 'string' && e.type === 'table') as TableElement[]); + const tableElements = page.groups.flatMap(group => group.elements.filter(e => typeof e !== 'string' && e.type === 'table')); const vegaScope = dataLoaderMarkdown(dataLoaders.filter(dl => dl.type !== 'spec'), variables, tableElements); @@ -91,7 +91,8 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve } else if (typeof element === 'object') { switch (element.type) { case 'chart': { - const chartFull = element.chart as ChartFull; + const { chart } = element; + const chartFull = chart as ChartFull; //see if it's a placeholder or a full chart if (!chartFull.spec) { //add a markdown element (not a chart element) with an image of the spinner at /img/chart-spinner.gif @@ -102,60 +103,65 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve break; } case 'checkbox': { + const { label, variableId } = element; const cbSpec: Plugins.CheckboxSpec = { - variableId: element.variableId, - value: variables.find(v => v.variableId === element.variableId)?.initialValue as boolean, - label: element.label, + variableId: variableId, + value: variables.find(v => v.variableId === variableId)?.initialValue as boolean, + label, }; mdElements.push(jsonWrap('checkbox', JSON.stringify(cbSpec, null, 2))); break; } case 'dropdown': { + const { label, variableId, options, dynamicOptions, multiple, size } = element; const ddSpec: Plugins.DropdownSpec = { - variableId: element.variableId, - value: variables.find(v => v.variableId === element.variableId)?.initialValue as string | string[], - label: element.label, + variableId, + value: variables.find(v => v.variableId === variableId)?.initialValue as string | string[], + label, }; - if (element.dynamicOptions) { + if (dynamicOptions) { ddSpec.dynamicOptions = { - dataSourceName: element.dynamicOptions.dataSourceName, - fieldName: element.dynamicOptions.fieldName, + dataSourceName: dynamicOptions.dataSourceName, + fieldName: dynamicOptions.fieldName, }; } else { - ddSpec.options = element.options; + ddSpec.options = options; } - if (element.multiple) { - ddSpec.multiple = element.multiple; - ddSpec.size = element.size || 1; + if (multiple) { + ddSpec.multiple = multiple; + ddSpec.size = size || 1; } mdElements.push(jsonWrap('dropdown', JSON.stringify(ddSpec, null, 2))); break; } case 'image': { - const urlSignal = vegaScope.createUrlSignal(element.urlRef); + const { urlRef, alt, width, height } = element; + const urlSignal = vegaScope.createUrlSignal(urlRef); const imageSpec: Plugins.ImageSpec = { srcSignalName: urlSignal.name, - alt: element.alt, - width: element.width, - height: element.height, + alt, + width, + height, }; mdElements.push(jsonWrap('image', JSON.stringify(imageSpec, null, 2))); break; } case 'presets': { - const presetsSpec: Plugins.PresetsSpec = element.presets; + const { presets } = element; + const presetsSpec: Plugins.PresetsSpec = presets; mdElements.push(jsonWrap('presets', JSON.stringify(presetsSpec, null, 2))); break; } case 'slider': { + const { label, min, max, step, variableId } = element; const sliderSpec: Plugins.SliderSpec = { - variableId: element.variableId, - value: variables.find(v => v.variableId === element.variableId)?.initialValue as number, - label: element.label, - min: element.min, - max: element.max, - step: element.step, + variableId, + value: variables.find(v => v.variableId === variableId)?.initialValue as number, + label, + min, + max, + step, }; mdElements.push(jsonWrap('slider', JSON.stringify(sliderSpec, null, 2))); break; @@ -167,12 +173,13 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve break; } case 'textbox': { - const textboxElement = element as TextboxElement; + const { variableId, label, multiline, placeholder } = element; const textboxSpec: Plugins.TextboxSpec = { - variableId: textboxElement.variableId, - value: variables.find(v => v.variableId === textboxElement.variableId)?.initialValue as string, - label: textboxElement.label, - multiline: textboxElement.multiline, + variableId, + value: variables.find(v => v.variableId === variableId)?.initialValue as string, + label, + multiline, + placeholder, }; mdElements.push(jsonWrap('textbox', JSON.stringify(textboxSpec, null, 2))); break; From 3a2319f02cb46fc25a7726c995d80cbc87cf94ce Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 18:26:52 -0700 Subject: [PATCH 59/61] rebuild --- docs/dist/idocs.compiler.umd.js | 67 ++++++++++++++++++--------------- docs/dist/idocs.editor.umd.js | 67 ++++++++++++++++++--------------- docs/dist/idocs.host.umd.js | 67 ++++++++++++++++++--------------- 3 files changed, 111 insertions(+), 90 deletions(-) diff --git a/docs/dist/idocs.compiler.umd.js b/docs/dist/idocs.compiler.umd.js index 8f0fb135..e8904310 100644 --- a/docs/dist/idocs.compiler.umd.js +++ b/docs/dist/idocs.compiler.umd.js @@ -294,7 +294,8 @@ ${content} } else if (typeof element === "object") { switch (element.type) { case "chart": { - const chartFull = element.chart; + const { chart } = element; + const chartFull = chart; if (!chartFull.spec) { mdElements.push("![Chart Spinner](/img/chart-spinner.gif)"); } else { @@ -303,59 +304,64 @@ ${content} break; } case "checkbox": { + const { label, variableId } = element; const cbSpec = { - variableId: element.variableId, - value: (_a = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _a.initialValue, - label: element.label + variableId, + value: (_a = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _a.initialValue, + label }; mdElements.push(jsonWrap("checkbox", JSON.stringify(cbSpec, null, 2))); break; } case "dropdown": { + const { label, variableId, options, dynamicOptions, multiple, size } = element; const ddSpec = { - variableId: element.variableId, - value: (_b = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _b.initialValue, - label: element.label + variableId, + value: (_b = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _b.initialValue, + label }; - if (element.dynamicOptions) { + if (dynamicOptions) { ddSpec.dynamicOptions = { - dataSourceName: element.dynamicOptions.dataSourceName, - fieldName: element.dynamicOptions.fieldName + dataSourceName: dynamicOptions.dataSourceName, + fieldName: dynamicOptions.fieldName }; } else { - ddSpec.options = element.options; + ddSpec.options = options; } - if (element.multiple) { - ddSpec.multiple = element.multiple; - ddSpec.size = element.size || 1; + if (multiple) { + ddSpec.multiple = multiple; + ddSpec.size = size || 1; } mdElements.push(jsonWrap("dropdown", JSON.stringify(ddSpec, null, 2))); break; } case "image": { - const urlSignal = vegaScope.createUrlSignal(element.urlRef); + const { urlRef, alt, width, height } = element; + const urlSignal = vegaScope.createUrlSignal(urlRef); const imageSpec = { srcSignalName: urlSignal.name, - alt: element.alt, - width: element.width, - height: element.height + alt, + width, + height }; mdElements.push(jsonWrap("image", JSON.stringify(imageSpec, null, 2))); break; } case "presets": { - const presetsSpec = element.presets; + const { presets } = element; + const presetsSpec = presets; mdElements.push(jsonWrap("presets", JSON.stringify(presetsSpec, null, 2))); break; } case "slider": { + const { label, min, max, step, variableId } = element; const sliderSpec = { - variableId: element.variableId, - value: (_c = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _c.initialValue, - label: element.label, - min: element.min, - max: element.max, - step: element.step + variableId, + value: (_c = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _c.initialValue, + label, + min, + max, + step }; mdElements.push(jsonWrap("slider", JSON.stringify(sliderSpec, null, 2))); break; @@ -367,12 +373,13 @@ ${content} break; } case "textbox": { - const textboxElement = element; + const { variableId, label, multiline, placeholder } = element; const textboxSpec = { - variableId: textboxElement.variableId, - value: (_d = variables.find((v) => v.variableId === textboxElement.variableId)) == null ? void 0 : _d.initialValue, - label: textboxElement.label, - multiline: textboxElement.multiline + variableId, + value: (_d = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _d.initialValue, + label, + multiline, + placeholder }; mdElements.push(jsonWrap("textbox", JSON.stringify(textboxSpec, null, 2))); break; diff --git a/docs/dist/idocs.editor.umd.js b/docs/dist/idocs.editor.umd.js index 1ac500b3..1b898b87 100644 --- a/docs/dist/idocs.editor.umd.js +++ b/docs/dist/idocs.editor.umd.js @@ -296,7 +296,8 @@ ${content} } else if (typeof element === "object") { switch (element.type) { case "chart": { - const chartFull = element.chart; + const { chart } = element; + const chartFull = chart; if (!chartFull.spec) { mdElements.push("![Chart Spinner](/img/chart-spinner.gif)"); } else { @@ -305,59 +306,64 @@ ${content} break; } case "checkbox": { + const { label, variableId } = element; const cbSpec = { - variableId: element.variableId, - value: (_a = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _a.initialValue, - label: element.label + variableId, + value: (_a = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _a.initialValue, + label }; mdElements.push(jsonWrap("checkbox", JSON.stringify(cbSpec, null, 2))); break; } case "dropdown": { + const { label, variableId, options, dynamicOptions, multiple, size } = element; const ddSpec = { - variableId: element.variableId, - value: (_b = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _b.initialValue, - label: element.label + variableId, + value: (_b = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _b.initialValue, + label }; - if (element.dynamicOptions) { + if (dynamicOptions) { ddSpec.dynamicOptions = { - dataSourceName: element.dynamicOptions.dataSourceName, - fieldName: element.dynamicOptions.fieldName + dataSourceName: dynamicOptions.dataSourceName, + fieldName: dynamicOptions.fieldName }; } else { - ddSpec.options = element.options; + ddSpec.options = options; } - if (element.multiple) { - ddSpec.multiple = element.multiple; - ddSpec.size = element.size || 1; + if (multiple) { + ddSpec.multiple = multiple; + ddSpec.size = size || 1; } mdElements.push(jsonWrap("dropdown", JSON.stringify(ddSpec, null, 2))); break; } case "image": { - const urlSignal = vegaScope.createUrlSignal(element.urlRef); + const { urlRef, alt, width, height } = element; + const urlSignal = vegaScope.createUrlSignal(urlRef); const imageSpec = { srcSignalName: urlSignal.name, - alt: element.alt, - width: element.width, - height: element.height + alt, + width, + height }; mdElements.push(jsonWrap("image", JSON.stringify(imageSpec, null, 2))); break; } case "presets": { - const presetsSpec = element.presets; + const { presets } = element; + const presetsSpec = presets; mdElements.push(jsonWrap("presets", JSON.stringify(presetsSpec, null, 2))); break; } case "slider": { + const { label, min, max, step, variableId } = element; const sliderSpec = { - variableId: element.variableId, - value: (_c = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _c.initialValue, - label: element.label, - min: element.min, - max: element.max, - step: element.step + variableId, + value: (_c = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _c.initialValue, + label, + min, + max, + step }; mdElements.push(jsonWrap("slider", JSON.stringify(sliderSpec, null, 2))); break; @@ -369,12 +375,13 @@ ${content} break; } case "textbox": { - const textboxElement = element; + const { variableId, label, multiline, placeholder } = element; const textboxSpec = { - variableId: textboxElement.variableId, - value: (_d = variables.find((v) => v.variableId === textboxElement.variableId)) == null ? void 0 : _d.initialValue, - label: textboxElement.label, - multiline: textboxElement.multiline + variableId, + value: (_d = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _d.initialValue, + label, + multiline, + placeholder }; mdElements.push(jsonWrap("textbox", JSON.stringify(textboxSpec, null, 2))); break; diff --git a/docs/dist/idocs.host.umd.js b/docs/dist/idocs.host.umd.js index f549ca02..b7247ab3 100644 --- a/docs/dist/idocs.host.umd.js +++ b/docs/dist/idocs.host.umd.js @@ -296,7 +296,8 @@ ${content} } else if (typeof element === "object") { switch (element.type) { case "chart": { - const chartFull = element.chart; + const { chart } = element; + const chartFull = chart; if (!chartFull.spec) { mdElements.push("![Chart Spinner](/img/chart-spinner.gif)"); } else { @@ -305,59 +306,64 @@ ${content} break; } case "checkbox": { + const { label, variableId } = element; const cbSpec = { - variableId: element.variableId, - value: (_a = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _a.initialValue, - label: element.label + variableId, + value: (_a = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _a.initialValue, + label }; mdElements.push(jsonWrap("checkbox", JSON.stringify(cbSpec, null, 2))); break; } case "dropdown": { + const { label, variableId, options, dynamicOptions, multiple, size } = element; const ddSpec = { - variableId: element.variableId, - value: (_b = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _b.initialValue, - label: element.label + variableId, + value: (_b = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _b.initialValue, + label }; - if (element.dynamicOptions) { + if (dynamicOptions) { ddSpec.dynamicOptions = { - dataSourceName: element.dynamicOptions.dataSourceName, - fieldName: element.dynamicOptions.fieldName + dataSourceName: dynamicOptions.dataSourceName, + fieldName: dynamicOptions.fieldName }; } else { - ddSpec.options = element.options; + ddSpec.options = options; } - if (element.multiple) { - ddSpec.multiple = element.multiple; - ddSpec.size = element.size || 1; + if (multiple) { + ddSpec.multiple = multiple; + ddSpec.size = size || 1; } mdElements.push(jsonWrap("dropdown", JSON.stringify(ddSpec, null, 2))); break; } case "image": { - const urlSignal = vegaScope.createUrlSignal(element.urlRef); + const { urlRef, alt, width, height } = element; + const urlSignal = vegaScope.createUrlSignal(urlRef); const imageSpec = { srcSignalName: urlSignal.name, - alt: element.alt, - width: element.width, - height: element.height + alt, + width, + height }; mdElements.push(jsonWrap("image", JSON.stringify(imageSpec, null, 2))); break; } case "presets": { - const presetsSpec = element.presets; + const { presets } = element; + const presetsSpec = presets; mdElements.push(jsonWrap("presets", JSON.stringify(presetsSpec, null, 2))); break; } case "slider": { + const { label, min, max, step, variableId } = element; const sliderSpec = { - variableId: element.variableId, - value: (_c = variables.find((v) => v.variableId === element.variableId)) == null ? void 0 : _c.initialValue, - label: element.label, - min: element.min, - max: element.max, - step: element.step + variableId, + value: (_c = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _c.initialValue, + label, + min, + max, + step }; mdElements.push(jsonWrap("slider", JSON.stringify(sliderSpec, null, 2))); break; @@ -369,12 +375,13 @@ ${content} break; } case "textbox": { - const textboxElement = element; + const { variableId, label, multiline, placeholder } = element; const textboxSpec = { - variableId: textboxElement.variableId, - value: (_d = variables.find((v) => v.variableId === textboxElement.variableId)) == null ? void 0 : _d.initialValue, - label: textboxElement.label, - multiline: textboxElement.multiline + variableId, + value: (_d = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _d.initialValue, + label, + multiline, + placeholder }; mdElements.push(jsonWrap("textbox", JSON.stringify(textboxSpec, null, 2))); break; From 500ce88bef58ec9ff5e7673203864e0e93967982 Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 18:57:27 -0700 Subject: [PATCH 60/61] use typed pluginNames --- docs/dist/idocs.compiler.umd.js | 17 +++++++------ docs/dist/idocs.editor.umd.js | 17 +++++++------ docs/dist/idocs.host.umd.js | 17 +++++++------ packages/compiler/src/md.ts | 24 ++++++++++++------- packages/markdown/src/plugins/checkbox.ts | 3 ++- packages/markdown/src/plugins/css.ts | 3 ++- packages/markdown/src/plugins/dropdown.ts | 3 ++- packages/markdown/src/plugins/image.ts | 3 ++- packages/markdown/src/plugins/interfaces.ts | 13 ++++++++++ packages/markdown/src/plugins/placeholders.ts | 3 ++- packages/markdown/src/plugins/presets.ts | 3 ++- packages/markdown/src/plugins/slider.ts | 7 +++--- packages/markdown/src/plugins/tabulator.ts | 3 ++- packages/markdown/src/plugins/textbox.ts | 7 +++--- packages/markdown/src/plugins/vega-lite.ts | 3 ++- packages/markdown/src/plugins/vega.ts | 3 ++- 16 files changed, 84 insertions(+), 45 deletions(-) diff --git a/docs/dist/idocs.compiler.umd.js b/docs/dist/idocs.compiler.umd.js index e8904310..fc1c3f90 100644 --- a/docs/dist/idocs.compiler.umd.js +++ b/docs/dist/idocs.compiler.umd.js @@ -288,6 +288,9 @@ ${content} function groupMarkdown(group, variables, vegaScope) { var _a, _b, _c, _d; const mdElements = []; + const addSpec = (pluginName, spec) => { + mdElements.push(jsonWrap(pluginName, JSON.stringify(spec, null, 2))); + }; for (const element of group.elements) { if (typeof element === "string") { mdElements.push(element); @@ -310,7 +313,7 @@ ${content} value: (_a = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _a.initialValue, label }; - mdElements.push(jsonWrap("checkbox", JSON.stringify(cbSpec, null, 2))); + addSpec("checkbox", cbSpec); break; } case "dropdown": { @@ -332,7 +335,7 @@ ${content} ddSpec.multiple = multiple; ddSpec.size = size || 1; } - mdElements.push(jsonWrap("dropdown", JSON.stringify(ddSpec, null, 2))); + addSpec("dropdown", ddSpec); break; } case "image": { @@ -344,13 +347,13 @@ ${content} width, height }; - mdElements.push(jsonWrap("image", JSON.stringify(imageSpec, null, 2))); + addSpec("image", imageSpec); break; } case "presets": { const { presets } = element; const presetsSpec = presets; - mdElements.push(jsonWrap("presets", JSON.stringify(presetsSpec, null, 2))); + addSpec("presets", presetsSpec); break; } case "slider": { @@ -363,13 +366,13 @@ ${content} max, step }; - mdElements.push(jsonWrap("slider", JSON.stringify(sliderSpec, null, 2))); + addSpec("slider", sliderSpec); break; } case "table": { const { dataSourceName, variableId, tabulatorOptions } = element; const tableSpec = { dataSourceName, variableId, tabulatorOptions }; - mdElements.push(jsonWrap("tabulator", JSON.stringify(tableSpec, null, 2))); + addSpec("tabulator", tableSpec); break; } case "textbox": { @@ -381,7 +384,7 @@ ${content} multiline, placeholder }; - mdElements.push(jsonWrap("textbox", JSON.stringify(textboxSpec, null, 2))); + addSpec("textbox", textboxSpec); break; } } diff --git a/docs/dist/idocs.editor.umd.js b/docs/dist/idocs.editor.umd.js index 1b898b87..efc3bdac 100644 --- a/docs/dist/idocs.editor.umd.js +++ b/docs/dist/idocs.editor.umd.js @@ -290,6 +290,9 @@ ${content} function groupMarkdown(group, variables, vegaScope) { var _a, _b, _c, _d; const mdElements = []; + const addSpec = (pluginName, spec) => { + mdElements.push(jsonWrap(pluginName, JSON.stringify(spec, null, 2))); + }; for (const element of group.elements) { if (typeof element === "string") { mdElements.push(element); @@ -312,7 +315,7 @@ ${content} value: (_a = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _a.initialValue, label }; - mdElements.push(jsonWrap("checkbox", JSON.stringify(cbSpec, null, 2))); + addSpec("checkbox", cbSpec); break; } case "dropdown": { @@ -334,7 +337,7 @@ ${content} ddSpec.multiple = multiple; ddSpec.size = size || 1; } - mdElements.push(jsonWrap("dropdown", JSON.stringify(ddSpec, null, 2))); + addSpec("dropdown", ddSpec); break; } case "image": { @@ -346,13 +349,13 @@ ${content} width, height }; - mdElements.push(jsonWrap("image", JSON.stringify(imageSpec, null, 2))); + addSpec("image", imageSpec); break; } case "presets": { const { presets } = element; const presetsSpec = presets; - mdElements.push(jsonWrap("presets", JSON.stringify(presetsSpec, null, 2))); + addSpec("presets", presetsSpec); break; } case "slider": { @@ -365,13 +368,13 @@ ${content} max, step }; - mdElements.push(jsonWrap("slider", JSON.stringify(sliderSpec, null, 2))); + addSpec("slider", sliderSpec); break; } case "table": { const { dataSourceName, variableId, tabulatorOptions } = element; const tableSpec = { dataSourceName, variableId, tabulatorOptions }; - mdElements.push(jsonWrap("tabulator", JSON.stringify(tableSpec, null, 2))); + addSpec("tabulator", tableSpec); break; } case "textbox": { @@ -383,7 +386,7 @@ ${content} multiline, placeholder }; - mdElements.push(jsonWrap("textbox", JSON.stringify(textboxSpec, null, 2))); + addSpec("textbox", textboxSpec); break; } } diff --git a/docs/dist/idocs.host.umd.js b/docs/dist/idocs.host.umd.js index b7247ab3..f51eb092 100644 --- a/docs/dist/idocs.host.umd.js +++ b/docs/dist/idocs.host.umd.js @@ -290,6 +290,9 @@ ${content} function groupMarkdown(group, variables, vegaScope) { var _a, _b, _c, _d; const mdElements = []; + const addSpec = (pluginName, spec) => { + mdElements.push(jsonWrap(pluginName, JSON.stringify(spec, null, 2))); + }; for (const element of group.elements) { if (typeof element === "string") { mdElements.push(element); @@ -312,7 +315,7 @@ ${content} value: (_a = variables.find((v) => v.variableId === variableId)) == null ? void 0 : _a.initialValue, label }; - mdElements.push(jsonWrap("checkbox", JSON.stringify(cbSpec, null, 2))); + addSpec("checkbox", cbSpec); break; } case "dropdown": { @@ -334,7 +337,7 @@ ${content} ddSpec.multiple = multiple; ddSpec.size = size || 1; } - mdElements.push(jsonWrap("dropdown", JSON.stringify(ddSpec, null, 2))); + addSpec("dropdown", ddSpec); break; } case "image": { @@ -346,13 +349,13 @@ ${content} width, height }; - mdElements.push(jsonWrap("image", JSON.stringify(imageSpec, null, 2))); + addSpec("image", imageSpec); break; } case "presets": { const { presets } = element; const presetsSpec = presets; - mdElements.push(jsonWrap("presets", JSON.stringify(presetsSpec, null, 2))); + addSpec("presets", presetsSpec); break; } case "slider": { @@ -365,13 +368,13 @@ ${content} max, step }; - mdElements.push(jsonWrap("slider", JSON.stringify(sliderSpec, null, 2))); + addSpec("slider", sliderSpec); break; } case "table": { const { dataSourceName, variableId, tabulatorOptions } = element; const tableSpec = { dataSourceName, variableId, tabulatorOptions }; - mdElements.push(jsonWrap("tabulator", JSON.stringify(tableSpec, null, 2))); + addSpec("tabulator", tableSpec); break; } case "textbox": { @@ -383,7 +386,7 @@ ${content} multiline, placeholder }; - mdElements.push(jsonWrap("textbox", JSON.stringify(textboxSpec, null, 2))); + addSpec("textbox", textboxSpec); break; } } diff --git a/packages/compiler/src/md.ts b/packages/compiler/src/md.ts index 55bd4aec..1eca1ce8 100644 --- a/packages/compiler/src/md.ts +++ b/packages/compiler/src/md.ts @@ -83,8 +83,15 @@ function dataLoaderMarkdown(dataSources: DataSource[], variables: Variable[], ta return vegaScope; } +type pluginSpecs = Plugins.CheckboxSpec | Plugins.DropdownSpec | Plugins.ImageSpec | Plugins.PresetsSpec | Plugins.SliderSpec | Plugins.TabulatorSpec | Plugins.TextboxSpec; + function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: VegaScope) { const mdElements: string[] = []; + + const addSpec = (pluginName: Plugins.PluginNames, spec: pluginSpecs) => { + mdElements.push(jsonWrap(pluginName, JSON.stringify(spec, null, 2))); + } + for (const element of group.elements) { if (typeof element === 'string') { mdElements.push(element); @@ -105,11 +112,11 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve case 'checkbox': { const { label, variableId } = element; const cbSpec: Plugins.CheckboxSpec = { - variableId: variableId, + variableId, value: variables.find(v => v.variableId === variableId)?.initialValue as boolean, label, }; - mdElements.push(jsonWrap('checkbox', JSON.stringify(cbSpec, null, 2))); + addSpec('checkbox', cbSpec); break; } case 'dropdown': { @@ -131,8 +138,7 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve ddSpec.multiple = multiple; ddSpec.size = size || 1; } - - mdElements.push(jsonWrap('dropdown', JSON.stringify(ddSpec, null, 2))); + addSpec('dropdown', ddSpec); break; } case 'image': { @@ -144,13 +150,13 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve width, height, }; - mdElements.push(jsonWrap('image', JSON.stringify(imageSpec, null, 2))); + addSpec('image', imageSpec); break; } case 'presets': { const { presets } = element; const presetsSpec: Plugins.PresetsSpec = presets; - mdElements.push(jsonWrap('presets', JSON.stringify(presetsSpec, null, 2))); + addSpec('presets', presetsSpec); break; } case 'slider': { @@ -163,13 +169,13 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve max, step, }; - mdElements.push(jsonWrap('slider', JSON.stringify(sliderSpec, null, 2))); + addSpec('slider', sliderSpec); break; } case 'table': { const { dataSourceName, variableId, tabulatorOptions } = element; const tableSpec: Plugins.TabulatorSpec = { dataSourceName, variableId, tabulatorOptions }; - mdElements.push(jsonWrap('tabulator', JSON.stringify(tableSpec, null, 2))); + addSpec('tabulator', tableSpec); break; } case 'textbox': { @@ -181,7 +187,7 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve multiline, placeholder, }; - mdElements.push(jsonWrap('textbox', JSON.stringify(textboxSpec, null, 2))); + addSpec('textbox', textboxSpec); break; } } diff --git a/packages/markdown/src/plugins/checkbox.ts b/packages/markdown/src/plugins/checkbox.ts index 0b2444cc..d5483ca7 100644 --- a/packages/markdown/src/plugins/checkbox.ts +++ b/packages/markdown/src/plugins/checkbox.ts @@ -7,6 +7,7 @@ import { VariableControl } from 'schema'; import { Batch, IInstance, Plugin } from '../factory.js'; import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; +import { PluginNames } from './interfaces.js'; interface CheckboxInstance { id: string; @@ -18,7 +19,7 @@ export interface CheckboxSpec extends VariableControl { value?: boolean; } -const pluginName = 'checkbox'; +const pluginName: PluginNames = 'checkbox'; const className = pluginClassName(pluginName); export const checkboxPlugin: Plugin = { diff --git a/packages/markdown/src/plugins/css.ts b/packages/markdown/src/plugins/css.ts index 487f6ca8..39235c2d 100644 --- a/packages/markdown/src/plugins/css.ts +++ b/packages/markdown/src/plugins/css.ts @@ -8,6 +8,7 @@ import { sanitizedHTML } from '../sanitize.js'; import * as Csstree from 'css-tree'; import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; +import { PluginNames } from './interfaces.js'; // CSS Tree is expected to be available as a global variable declare const csstree: typeof Csstree; @@ -312,7 +313,7 @@ function categorizeCss(cssContent: string) { return result; } -const pluginName = 'css'; +const pluginName: PluginNames = 'css'; const className = pluginClassName(pluginName); export const cssPlugin: Plugin = { diff --git a/packages/markdown/src/plugins/dropdown.ts b/packages/markdown/src/plugins/dropdown.ts index cb98d5af..d7be9329 100644 --- a/packages/markdown/src/plugins/dropdown.ts +++ b/packages/markdown/src/plugins/dropdown.ts @@ -7,6 +7,7 @@ import { DropdownElementProps } from 'schema'; import { Batch, IInstance, Plugin } from '../factory.js'; import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; +import { PluginNames } from './interfaces.js'; interface DropdownInstance { id: string; @@ -18,7 +19,7 @@ export interface DropdownSpec extends DropdownElementProps { value?: string | string[]; } -const pluginName = 'dropdown'; +const pluginName: PluginNames = 'dropdown'; const className = pluginClassName(pluginName); export const dropdownPlugin: Plugin = { diff --git a/packages/markdown/src/plugins/image.ts b/packages/markdown/src/plugins/image.ts index fb57f275..5a0cbea0 100644 --- a/packages/markdown/src/plugins/image.ts +++ b/packages/markdown/src/plugins/image.ts @@ -7,6 +7,7 @@ import { ImageElementProps } from 'schema'; import { IInstance, Plugin } from '../factory.js'; import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; +import { PluginNames } from './interfaces.js'; export interface ImageSpec extends ImageElementProps { srcSignalName: string; @@ -25,7 +26,7 @@ enum ImageOpacity { error = '0.5', } -const pluginName = 'image'; +const pluginName: PluginNames = 'image'; const className = pluginClassName(pluginName); export const imagePlugin: Plugin = { diff --git a/packages/markdown/src/plugins/interfaces.ts b/packages/markdown/src/plugins/interfaces.ts index 916ab983..6241ee33 100644 --- a/packages/markdown/src/plugins/interfaces.ts +++ b/packages/markdown/src/plugins/interfaces.ts @@ -5,3 +5,16 @@ export { PresetsSpec } from './presets.js'; export { SliderSpec } from './slider.js'; export { TabulatorSpec } from './tabulator.js'; export { TextboxSpec } from './textbox.js'; + +export type PluginNames = + 'css' | + 'checkbox' | + 'dropdown' | + 'image' | + 'placeholders' | + 'presets' | + 'slider' | + 'tabulator' | + 'textbox' | + 'vega-lite' | + 'vega'; diff --git a/packages/markdown/src/plugins/placeholders.ts b/packages/markdown/src/plugins/placeholders.ts index fefa2fbd..ec9038e4 100644 --- a/packages/markdown/src/plugins/placeholders.ts +++ b/packages/markdown/src/plugins/placeholders.ts @@ -5,6 +5,7 @@ import { Token } from 'markdown-it/index.js'; import { Batch, IInstance, Plugin, PrioritizedSignal } from '../factory.js'; +import { PluginNames } from './interfaces.js'; function createTemplateFunction(template: string) { const parts = template.split(/(%7B%7B.*?%7D%7D)/g).map(part => { @@ -34,7 +35,7 @@ function handleDynamicUrl(tokens: Token[], idx: number, attrName: string, elemen return token; } -const pluginName = 'placeholders'; +const pluginName: PluginNames = 'placeholders'; export const placeholdersPlugin: Plugin = { name: pluginName, diff --git a/packages/markdown/src/plugins/presets.ts b/packages/markdown/src/plugins/presets.ts index 6549830c..696753d9 100644 --- a/packages/markdown/src/plugins/presets.ts +++ b/packages/markdown/src/plugins/presets.ts @@ -7,6 +7,7 @@ import { Preset } from 'schema'; import { Batch, IInstance, Plugin, PrioritizedSignal } from '../factory.js'; import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; +import { PluginNames } from './interfaces.js'; export type PresetsSpec = Preset[]; @@ -16,7 +17,7 @@ interface PresetsInstance { element: HTMLUListElement; } -const pluginName = 'presets'; +const pluginName: PluginNames = 'presets'; const className = pluginClassName(pluginName); export const presetsPlugin: Plugin = { diff --git a/packages/markdown/src/plugins/slider.ts b/packages/markdown/src/plugins/slider.ts index a572b257..a9298126 100644 --- a/packages/markdown/src/plugins/slider.ts +++ b/packages/markdown/src/plugins/slider.ts @@ -7,6 +7,7 @@ import { VariableControl, SliderElementProps } from 'schema'; import { Batch, IInstance, Plugin } from '../factory.js'; import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; +import { PluginNames } from './interfaces.js'; interface SliderInstance { id: string; @@ -18,7 +19,7 @@ export interface SliderSpec extends VariableControl, SliderElementProps { value?: number; } -const pluginName = 'slider'; +const pluginName: PluginNames = 'slider'; const className = pluginClassName(pluginName); export const sliderPlugin: Plugin = { @@ -54,7 +55,7 @@ export const sliderPlugin: Plugin = { const instances: IInstance[] = sliderInstances.map((sliderInstance) => { const { element, spec } = sliderInstance; const valueSpan = element.parentElement?.querySelector('.vega-bind-value') as HTMLSpanElement; - + const initialSignals = [{ name: spec.variableId, value: spec.value || spec.min, @@ -89,7 +90,7 @@ export const sliderPlugin: Plugin = { }; renderer.signalBus.broadcast(sliderInstance.id, batch); }; - + element.addEventListener('input', updateValue); element.addEventListener('change', updateValue); }, diff --git a/packages/markdown/src/plugins/tabulator.ts b/packages/markdown/src/plugins/tabulator.ts index daa21d34..9c633974 100644 --- a/packages/markdown/src/plugins/tabulator.ts +++ b/packages/markdown/src/plugins/tabulator.ts @@ -8,6 +8,7 @@ import { Tabulator as TabulatorType, Options as TabulatorOptions } from 'tabulat import { pluginClassName } from './util.js'; import { TableElementProps } from 'schema'; import { flaggableJsonPlugin } from './config.js'; +import { PluginNames } from './interfaces.js'; interface TabulatorInstance { id: string; @@ -30,7 +31,7 @@ export function inspectTabulatorSpec(spec: TabulatorSpec) { return flaggableSpec; } -const pluginName = 'tabulator'; +const pluginName: PluginNames = 'tabulator'; const className = pluginClassName(pluginName); export const tabulatorPlugin: Plugin = { diff --git a/packages/markdown/src/plugins/textbox.ts b/packages/markdown/src/plugins/textbox.ts index 6f454481..88208b49 100644 --- a/packages/markdown/src/plugins/textbox.ts +++ b/packages/markdown/src/plugins/textbox.ts @@ -7,6 +7,7 @@ import { TextboxElementProps } from 'schema'; import { Batch, IInstance, Plugin } from '../factory.js'; import { pluginClassName } from './util.js'; import { flaggableJsonPlugin } from './config.js'; +import { PluginNames } from './interfaces.js'; interface TextboxInstance { id: string; @@ -18,7 +19,7 @@ export interface TextboxSpec extends TextboxElementProps { value?: string; } -const pluginName = 'textbox'; +const pluginName: PluginNames = 'textbox'; const className = pluginClassName(pluginName); export const textboxPlugin: Plugin = { @@ -35,7 +36,7 @@ export const textboxPlugin: Plugin = { const spec: TextboxSpec = specReview.approvedSpec; const placeholderAttr = spec.placeholder ? ` placeholder="${spec.placeholder}"` : ''; - const inputElement = spec.multiline + const inputElement = spec.multiline ? `` : ``; @@ -84,7 +85,7 @@ export const textboxPlugin: Plugin = { }; renderer.signalBus.broadcast(textboxInstance.id, batch); }; - + element.addEventListener('input', updateValue); element.addEventListener('change', updateValue); }, diff --git a/packages/markdown/src/plugins/vega-lite.ts b/packages/markdown/src/plugins/vega-lite.ts index e7d9dd71..f3adc534 100644 --- a/packages/markdown/src/plugins/vega-lite.ts +++ b/packages/markdown/src/plugins/vega-lite.ts @@ -10,8 +10,9 @@ import { pluginClassName } from './util.js'; import { inspectVegaSpec, vegaPlugin } from './vega.js'; import { compile, TopLevelSpec } from 'vega-lite'; import { Spec } from 'vega'; +import { PluginNames } from './interfaces.js'; -const pluginName = 'vega-lite'; +const pluginName: PluginNames = 'vega-lite'; const className = pluginClassName(pluginName); export const vegaLitePlugin: Plugin = { diff --git a/packages/markdown/src/plugins/vega.ts b/packages/markdown/src/plugins/vega.ts index 01573743..1892f795 100644 --- a/packages/markdown/src/plugins/vega.ts +++ b/packages/markdown/src/plugins/vega.ts @@ -11,6 +11,7 @@ import { LogLevel } from '../signalbus.js'; import { pluginClassName, urlParam } from './util.js'; import { defaultCommonOptions } from 'common'; import { flaggableJsonPlugin, } from './config.js'; +import { PluginNames } from './interfaces.js'; const ignoredSignals = ['width', 'height', 'padding', 'autosize', 'background', 'style', 'parent', 'datum', 'item', 'event', 'cursor']; @@ -29,7 +30,7 @@ interface VegaInstance extends SpecInit { needToRun?: boolean; } -const pluginName = 'vega'; +const pluginName: PluginNames = 'vega'; const className = pluginClassName(pluginName); export function inspectVegaSpec(spec: Spec) { From 7a8ce2b6f74e26d1ed88b8da4049e12a33cee45a Mon Sep 17 00:00:00 2001 From: Dan Marshall Date: Mon, 28 Jul 2025 19:05:23 -0700 Subject: [PATCH 61/61] unpack object --- docs/dist/idocs.compiler.umd.js | 5 +++-- docs/dist/idocs.editor.umd.js | 5 +++-- docs/dist/idocs.host.umd.js | 5 +++-- packages/compiler/src/md.ts | 5 +++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/dist/idocs.compiler.umd.js b/docs/dist/idocs.compiler.umd.js index fc1c3f90..a257c689 100644 --- a/docs/dist/idocs.compiler.umd.js +++ b/docs/dist/idocs.compiler.umd.js @@ -324,9 +324,10 @@ ${content} label }; if (dynamicOptions) { + const { dataSourceName, fieldName } = dynamicOptions; ddSpec.dynamicOptions = { - dataSourceName: dynamicOptions.dataSourceName, - fieldName: dynamicOptions.fieldName + dataSourceName, + fieldName }; } else { ddSpec.options = options; diff --git a/docs/dist/idocs.editor.umd.js b/docs/dist/idocs.editor.umd.js index efc3bdac..97b62e58 100644 --- a/docs/dist/idocs.editor.umd.js +++ b/docs/dist/idocs.editor.umd.js @@ -326,9 +326,10 @@ ${content} label }; if (dynamicOptions) { + const { dataSourceName, fieldName } = dynamicOptions; ddSpec.dynamicOptions = { - dataSourceName: dynamicOptions.dataSourceName, - fieldName: dynamicOptions.fieldName + dataSourceName, + fieldName }; } else { ddSpec.options = options; diff --git a/docs/dist/idocs.host.umd.js b/docs/dist/idocs.host.umd.js index f51eb092..2368f244 100644 --- a/docs/dist/idocs.host.umd.js +++ b/docs/dist/idocs.host.umd.js @@ -326,9 +326,10 @@ ${content} label }; if (dynamicOptions) { + const { dataSourceName, fieldName } = dynamicOptions; ddSpec.dynamicOptions = { - dataSourceName: dynamicOptions.dataSourceName, - fieldName: dynamicOptions.fieldName + dataSourceName, + fieldName }; } else { ddSpec.options = options; diff --git a/packages/compiler/src/md.ts b/packages/compiler/src/md.ts index 1eca1ce8..1e798396 100644 --- a/packages/compiler/src/md.ts +++ b/packages/compiler/src/md.ts @@ -127,9 +127,10 @@ function groupMarkdown(group: ElementGroup, variables: Variable[], vegaScope: Ve label, }; if (dynamicOptions) { + const { dataSourceName, fieldName } = dynamicOptions; ddSpec.dynamicOptions = { - dataSourceName: dynamicOptions.dataSourceName, - fieldName: dynamicOptions.fieldName, + dataSourceName, + fieldName, }; } else { ddSpec.options = options;