Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
036d221
Initial plan
Copilot Oct 24, 2025
e05d3af
Add JSON plugin for inline JSON data
Copilot Oct 24, 2025
0ab0f42
Update compiler to handle JSON format in dataLoaders
Copilot Oct 24, 2025
1a429b2
Add comprehensive JSON plugin example and documentation
Copilot Oct 24, 2025
ab43fa5
Update JSON plugin to use 'json data variableId' syntax and script tag
Copilot Oct 24, 2025
e38425f
Rename json plugin to json-data and fix loader.ts JSON handling
Copilot Oct 24, 2025
a67e6f8
Fix JSON dataLoaders to always output as fence blocks and rename types
Copilot Oct 24, 2025
fb01c3a
Rename plugin from json-data to data, remove special case logic
Copilot Oct 24, 2025
7f22663
Fix plugin name extraction to use infoWords instead of slice
Copilot Oct 24, 2025
7bede40
Preserve signal bus state during reset by carrying over logLevel and …
danmarshall Oct 24, 2025
8156077
handle nullref
danmarshall Oct 24, 2025
194451d
Fix plugin name extraction to check for 'json' keyword directly in in…
danmarshall Oct 24, 2025
99e01cf
receive initial batches without running
danmarshall Oct 27, 2025
687d3b4
Optimize initial batch handling in SignalBus by reusing the construct…
danmarshall Oct 27, 2025
f93a424
remove json data examples
danmarshall Oct 27, 2025
69ab583
Add YAML support to data plugin
Copilot Oct 27, 2025
58fbd35
Refactor Mermaid diagram rendering to use MermaidInstance for improve…
danmarshall Oct 27, 2025
78ba3d5
Improve batch handling in Vega plugin by setting needToRun flag based…
danmarshall Oct 27, 2025
0a92c75
Revert UMD build artifacts to exclude from PR
Copilot Nov 19, 2025
06f6174
Revert loader.ts changes and add UMD file guidance to copilot instruc…
Copilot Nov 20, 2025
065dd50
Rename data plugin to value plugin
Copilot Nov 20, 2025
80501c1
Add value plugin documentation to schema
Copilot Nov 20, 2025
fe002f9
Revert built schema file and add value plugin docs to source schema
Copilot Nov 20, 2025
dc5700e
Add ValueElement, CsvElement, TsvElement, DsvElement to schema
Copilot Nov 20, 2025
3e83113
Fix schema: revert built files, update types, move inline data to pag…
Copilot Nov 20, 2025
e70d759
Polish schema and compiler: minimal comments, arrays for content, val…
Copilot Nov 20, 2025
8ae7b85
Revert built files (UMD bundles and schema files) from PR
Copilot Nov 20, 2025
c661a6e
Remove built schema files and interactive.ts from PR
Copilot Nov 20, 2025
7724b46
Remove docs/schema/idoc_v1.json from PR
Copilot Nov 20, 2025
cef5728
Refactor DSV parsing functions to consolidate variable ID and delimit…
danmarshall Nov 21, 2025
ac90041
Update ignoredSignals to replace 'origins' with 'value'
danmarshall Nov 21, 2025
6af707d
Add parseFenceInfo function to extract metadata from fence info strings
danmarshall Nov 21, 2025
1e21dc2
Add default 'value' plugin fallback for JSON and YAML data in create …
danmarshall Nov 21, 2025
5e89f66
Refactor value plugin to use parseFenceInfo for improved fence info p…
danmarshall Nov 21, 2025
df9d27c
Refactor parseDsvInfo to utilize parseFenceInfo for improved paramete…
danmarshall Nov 21, 2025
e7b1087
Refactor JSON and YAML element types to remove '-value' suffix for co…
danmarshall Nov 21, 2025
7028155
alphabetize
danmarshall Nov 21, 2025
62b2706
common header
danmarshall Nov 21, 2025
06ca7ae
remove comments
danmarshall Nov 21, 2025
c6a1334
remove csv etc
danmarshall Nov 21, 2025
510ed09
convert to data loaders
danmarshall Nov 21, 2025
7d93eb4
fix: update type from "yaml-value" to "yaml" in YAML Value Plugin test
danmarshall Nov 21, 2025
94fe2e6
refactor: remove YAML Value Plugin test JSON file
danmarshall Nov 21, 2025
1a12e45
refactor: remove InlineDataElement and related exports
danmarshall Nov 21, 2025
82edd3c
refactor: remove JSON and YAML handling from groupMarkdown function
danmarshall Nov 21, 2025
9fa8a84
refactor: remove JSON and YAML element validation from validateElemen…
danmarshall Nov 21, 2025
2169e7b
remove json
danmarshall Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/dist/v1/chartifact.compiler.umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
inlineDataMd = tickWrap(`dsv delimiter:${delimiter} variableId:${ds_raw}`, content);
break;
}
case "json": {
inlineDataMd = tickWrap(`json ${ds_raw}`, content);
break;
}
default: {
console.warn(`Unsupported inline data format: ${dataSource.format}, type is ${typeof dataSource.content}`);
break;
Expand Down
4 changes: 4 additions & 0 deletions docs/dist/v1/chartifact.editor.umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
inlineDataMd = tickWrap(`dsv delimiter:${delimiter} variableId:${ds_raw}`, content);
break;
}
case "json": {
inlineDataMd = tickWrap(`json ${ds_raw}`, content);
break;
}
default: {
console.warn(`Unsupported inline data format: ${dataSource.format}, type is ${typeof dataSource.content}`);
break;
Expand Down
4 changes: 4 additions & 0 deletions docs/dist/v1/chartifact.host.umd.js
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
inlineDataMd = tickWrap(`dsv delimiter:${delimiter} variableId:${ds_raw}`, content);
break;
}
case "json": {
inlineDataMd = tickWrap(`json ${ds_raw}`, content);
break;
}
default: {
console.warn(`Unsupported inline data format: ${dataSource.format}, type is ${typeof dataSource.content}`);
break;
Expand Down
234 changes: 188 additions & 46 deletions docs/dist/v1/chartifact.markdown.umd.js

Large diffs are not rendered by default.

45 changes: 36 additions & 9 deletions packages/compiler/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,42 @@ export function addStaticDataLoaderToSpec(vegaScope: VegaScope, dataSource: Data

if (dataSource.type === 'inline' && dataSource.format === 'json') {

const newData: ValuesData = {
name: dataSourceName,
values: dataSource.content as object[],
transform: dataSource.dataFrameTransformations || [],
};
spec.signals.push(dataAsSignal(dataSourceName));

//real data goes to the beginning of the data array
spec.data.unshift(newData);
// Check if content is already an object array
if (Array.isArray(dataSource.content) && typeof dataSource.content[0] === 'object') {
const newData: ValuesData = {
name: dataSourceName,
values: dataSource.content as object[],

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot but we don't want it embedded with the spec, we want it output as tickWrap(json data ${ds_raw}, content); here too

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit a67e6f8:

  • JSON object arrays now stringified with JSON.stringify(content, null, 2) and output as tickWrap('json data ${ds_raw}', content)
  • No longer embedded directly in Vega spec
  • Consistent with CSV/TSV/DSV behavior

Also renamed types per new requirement:

  • JsonInstanceJsonDataInstance
  • JsonSpecJsonDataSpec

transform: dataSource.dataFrameTransformations || [],
};
spec.signals.push(dataAsSignal(dataSourceName));

//real data goes to the beginning of the data array
spec.data.unshift(newData);
} else if (typeof dataSource.content === 'string') {
// Content is JSON string - need to output as json data fence block
const content = dataSource.content;
let ds_raw = dataSourceName;

if (dataSource.dataFrameTransformations) {
ds_raw += '_raw';

const newData: SourceData = {
name: dataSourceName,
source: ds_raw,
transform: dataSource.dataFrameTransformations || [],
};
spec.signals.push(dataAsSignal(dataSourceName));

spec.data.unshift(newData);

//add a placeholder data since the transform depends on it
spec.data.unshift({
name: ds_raw
});
}

inlineDataMd = tickWrap(`json data ${ds_raw}`, content);
}

} else if (typeof dataSource.content === 'string' || (Array.isArray(dataSource.content) && typeof dataSource.content[0] === 'string')) {

Expand Down
8 changes: 8 additions & 0 deletions packages/markdown/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ export function create() {
else {
const infoWords = info.split(/\s+/);
if (infoWords.length > 0) {
// Special case for "json data" - treat as single plugin name
if (infoWords[0] === 'json' && infoWords[1] === 'data') {
const jsonDataPlugin = findPlugin('json-data');
if (jsonDataPlugin) {
return jsonDataPlugin;
}
}

const pluginPrefix = findPluginByPrefix(infoWords[0]);
if (pluginPrefix) {
return pluginPrefix;
Expand Down
2 changes: 2 additions & 0 deletions packages/markdown/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { dsvPlugin } from './dsv.js';
import { googleFontsPlugin } from './google-fonts.js';
import { dropdownPlugin } from './dropdown.js';
import { imagePlugin } from './image.js';
import { jsonDataPlugin } from './json-data.js';
import { mermaidPlugin } from './mermaid.js';
import { numberPlugin } from './number.js';
import { placeholdersPlugin } from './placeholders.js';
Expand All @@ -34,6 +35,7 @@ export function registerNativePlugins() {
registerMarkdownPlugin(googleFontsPlugin);
registerMarkdownPlugin(dropdownPlugin);
registerMarkdownPlugin(imagePlugin);
registerMarkdownPlugin(jsonDataPlugin);
registerMarkdownPlugin(mermaidPlugin);
registerMarkdownPlugin(numberPlugin);
registerMarkdownPlugin(placeholdersPlugin);
Expand Down
2 changes: 2 additions & 0 deletions packages/markdown/src/plugins/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { CsvSpec } from './csv.js';
export { DropdownSpec } from './dropdown.js';
export { DsvSpec } from './dsv.js';
export { ImageSpec } from './image.js';
export { JsonSpec } from './json-data.js';
export { MermaidSpec } from './mermaid.js';
export { NumberSpec } from './number.js';
export { PresetsSpec } from './presets.js';
Expand All @@ -25,6 +26,7 @@ export type PluginNames =
'dsv' |
'image' |
'google-fonts' |
'json-data' |
'mermaid' |
'number' |
'placeholders' |
Expand Down
216 changes: 216 additions & 0 deletions packages/markdown/src/plugins/json-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
/**
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT License.
*/

import { Batch, IInstance, Plugin, RawFlaggableSpec } from '../factory.js';
import { sanitizedScriptTag, sanitizeHtmlComment } from '../sanitize.js';
import { pluginClassName } from './util.js';
import { PluginNames } from './interfaces.js';
import { SpecReview } from 'common';
import { parseVariableId } from './dsv.js';

interface JsonInstance {
id: string;
spec: JsonSpec;
data: object[];
}

export interface JsonSpec {
variableId: string;
wasDefaultId?: boolean;
}

function inspectJsonSpec(spec: JsonSpec): RawFlaggableSpec<JsonSpec> {
const result: RawFlaggableSpec<JsonSpec> = {
spec,
hasFlags: false,
reasons: []
};

// Flag if we had to use defaults
if (spec.wasDefaultId) {
result.hasFlags = true;
result.reasons.push('No variable ID specified - using default');
}

return result;
}

const pluginName: PluginNames = 'json-data';
const className = pluginClassName(pluginName);

export const jsonDataPlugin: Plugin<JsonSpec> = {
name: pluginName,
fence: (token, index) => {
const content = token.content.trim();
const info = token.info.trim();

// Parse the fence info - expect "json data variableId" format
const parts = info.split(/\s+/);

// Require "json data" prefix
if (parts.length < 2 || parts[0] !== 'json' || parts[1] !== 'data') {
// This fence is not for json-data plugin
return '';
}

// Check for variable ID
let variableId: string;
let wasDefaultId = false;

if (parts.length >= 3) {
// Format: json data variableId
variableId = parts[2];
} else {
// Default variable ID if not provided
variableId = `jsonData${index}`;
wasDefaultId = true;
}

// Use script tag with application/json type instead of pre tag
const scriptElement = sanitizedScriptTag(content, {
id: `${pluginName}-${index}`,
class: className,
'data-variable-id': variableId,
'data-was-default-id': wasDefaultId.toString()
});

return scriptElement.outerHTML;
},
hydrateSpecs: (renderer, errorHandler) => {
const flagged: SpecReview<JsonSpec>[] = [];
const containers = renderer.element.querySelectorAll(`.${className}`);

for (const [index, container] of Array.from(containers).entries()) {
try {
const variableId = container.getAttribute('data-variable-id');
const wasDefaultId = container.getAttribute('data-was-default-id') === 'true';

if (!variableId) {
errorHandler(new Error('No variable ID found'), pluginName, index, 'parse', container);
continue;
}

const spec: JsonSpec = { variableId, wasDefaultId };
const flaggableSpec = inspectJsonSpec(spec);

const f: SpecReview<JsonSpec> = {
approvedSpec: null,
pluginName,
containerId: container.id
};

if (flaggableSpec.hasFlags) {
f.blockedSpec = flaggableSpec.spec;
f.reason = flaggableSpec.reasons?.join(', ') || 'Unknown reason';
} else {
f.approvedSpec = flaggableSpec.spec;
}

flagged.push(f);
} catch (e) {
errorHandler(e instanceof Error ? e : new Error(String(e)), pluginName, index, 'parse', container);
}
}

return flagged;
},
hydrateComponent: async (renderer, errorHandler, specs) => {
const { signalBus } = renderer;
const jsonInstances: JsonInstance[] = [];

for (let index = 0; index < specs.length; index++) {
const specReview = specs[index];
if (!specReview.approvedSpec) {
continue;
}

const container = renderer.element.querySelector(`#${specReview.containerId}`);
if (!container) {
errorHandler(new Error('Container not found'), pluginName, index, 'init', null);
continue;
}

try {
const content = container.textContent?.trim();
if (!content) {
errorHandler(new Error('No JSON content found'), pluginName, index, 'parse', container);
continue;
}

const spec: JsonSpec = specReview.approvedSpec;

// Parse JSON content
let data: object[];
try {
const parsed = JSON.parse(content);
// Ensure data is an array
if (Array.isArray(parsed)) {
data = parsed;
} else {
// If it's a single object, wrap it in an array
data = [parsed];
}
} catch (jsonError) {
errorHandler(
new Error(`Invalid JSON: ${jsonError instanceof Error ? jsonError.message : String(jsonError)}`),
pluginName,
index,
'parse',
container
);
continue;
}

const jsonInstance: JsonInstance = {
id: `${pluginName}-${index}`,
spec,
data
};
jsonInstances.push(jsonInstance);

// Add a safe comment before the container to show that data was loaded
const comment = sanitizeHtmlComment(`JSON data loaded: ${data.length} rows for variable '${spec.variableId}'`);
container.insertAdjacentHTML('beforebegin', comment);

} catch (e) {
errorHandler(e instanceof Error ? e : new Error(String(e)), pluginName, index, 'parse', container);
}
}

const instances = jsonInstances.map((jsonInstance): IInstance => {
const { spec, data } = jsonInstance;

const initialSignals = [{
name: spec.variableId,
value: data,
priority: 1,
isData: true,
}];

return {
...jsonInstance,
initialSignals,
beginListening() {
// JSON data is static, but we broadcast it when listening begins
const batch: Batch = {
[spec.variableId]: {
value: data,
isData: true,
},
};
signalBus.broadcast(jsonInstance.id, batch);
},
getCurrentSignalValue: () => {
return data;
},
destroy: () => {
// No cleanup needed for JSON data
},
};
});

return instances;
},
};
30 changes: 25 additions & 5 deletions packages/markdown/src/sanitize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,7 @@ export function sanitizedHTML(tagName: string, attributes: { [key: string]: stri

if (precedeWithScriptTag) {
// Create a script tag that precedes the main element
const scriptElement = domDocument.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;
const scriptElement = sanitizedScriptTag(content);

// Return script tag followed by empty element
return scriptElement.outerHTML + element.outerHTML;
Expand All @@ -43,6 +39,30 @@ export function sanitizedHTML(tagName: string, attributes: { [key: string]: stri
return element.outerHTML;
}

export function sanitizedScriptTag(content: string, attributes?: { [key: string]: string }): HTMLScriptElement {
if (!domDocument) {
throw new Error('No DOM Document available. Please set domDocument using setDomDocument.');
}

const scriptElement = domDocument.createElement('script');

// Set default type to application/json
scriptElement.setAttribute('type', 'application/json');

// Set additional attributes if provided
if (attributes) {
Object.keys(attributes).forEach(key => {
scriptElement.setAttribute(key, attributes[key]);
});
}

// Only escape the dangerous sequence that could break out of script tag
const safeContent = content.replace(/<\/script>/gi, '<\\/script>');
scriptElement.innerHTML = safeContent;

return scriptElement;
}

export function sanitizeHtmlComment(content: string) {

// First escape the content safely
Expand Down
Loading