diff --git a/README.md b/README.md index f65e631a..3bc93913 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ The Chartifact GitHub repo has source code for these interoperating modules: * **Inputs** – Textboxes, checkboxes, sliders, dropdowns * **Tables** – Sortable, selectable, and editable data grids * **Charts** – Vega and Vega-Lite visualizations + * **Diagrams** – Mermaid diagrams (flowcharts, networks, and more) via the mermaid plugin, including tabular data-driven diagram generation * **Images** – Dynamic image URLs based on variables * **Presets** – Named sets of variable values for quick scenario switching diff --git a/docs/assets/examples/features.markdown.folder.json b/docs/assets/examples/features.markdown.folder.json index 90ad2c31..f2b7a206 100644 --- a/docs/assets/examples/features.markdown.folder.json +++ b/docs/assets/examples/features.markdown.folder.json @@ -34,6 +34,10 @@ "title": "Presets", "href": "markdown/features/8.presets.idoc.md" }, + { + "title": "Mermaid", + "href": "markdown/features/9.mermaid.idoc.md" + }, { "title": "Data URL", "href": "markdown/features/data-url.idoc.md" diff --git a/docs/assets/examples/json/features/9.mermaid.idoc.json b/docs/assets/examples/json/features/9.mermaid.idoc.json new file mode 100644 index 00000000..76f591ae --- /dev/null +++ b/docs/assets/examples/json/features/9.mermaid.idoc.json @@ -0,0 +1,277 @@ +{ + "$schema": "https://microsoft.github.io/chartifact/schema/idoc_v1.json", + "title": "Feature: Mermaid Plugin", + "variables": [ + { + "variableId": "flowchartOutput", + "type": "string", + "initialValue": "" + }, + { + "variableId": "networkOutput", + "type": "string", + "initialValue": "" + } + ], + "dataLoaders": [ + { + "dataSourceName": "jsonData", + "type": "inline", + "format": "json", + "content": [ + { + "lineTemplate": "node", + "id": "A", + "label": "Start" + }, + { + "lineTemplate": "node", + "id": "B", + "label": "Middle" + }, + { + "lineTemplate": "node", + "id": "C", + "label": "End" + }, + { + "lineTemplate": "labeledEdge", + "from": "A", + "to": "B", + "label": "Next" + }, + { + "lineTemplate": "edge", + "from": "B", + "to": "C" + } + ] + }, + { + "dataSourceName": "networkData", + "type": "inline", + "format": "json", + "content": [ + { + "lineTemplate": "subgraph", + "name": "Production" + }, + { + "lineTemplate": "server", + "id": "web1", + "name": "Web Server", + "ip": "10.0.1.10" + }, + { + "lineTemplate": "server", + "id": "db1", + "name": "Database", + "ip": "10.0.1.20" + }, + { + "lineTemplate": "end" + }, + { + "lineTemplate": "subgraph", + "name": "Development" + }, + { + "lineTemplate": "server", + "id": "dev1", + "name": "Dev Server", + "ip": "10.0.2.10" + }, + { + "lineTemplate": "end" + }, + { + "lineTemplate": "secureConnection", + "from": "web1", + "to": "db1" + }, + { + "lineTemplate": "connection", + "from": "dev1", + "to": "web1" + } + ] + } + ], + "groups": [ + { + "groupId": "intro", + "elements": [ + "# Mermaid Plugin Examples", + "This page demonstrates the mermaid plugin in Chartifact, including raw text, data-driven, and dynamic modes." + ] + }, + { + "groupId": "raw-text", + "elements": [ + "## Raw Text Mode", + "Simple flowchart using raw mermaid syntax:", + { + "type": "mermaid", + "diagramText": "flowchart TD\n A[Start] --> B{Is it?}\n B -->|Yes| C[OK]\n B -->|No| D[End]\n C --> D" + } + ] + }, + { + "groupId": "data-driven", + "elements": [ + "## Data-Driven Mode", + "Template-based diagram generation:", + { + "type": "table", + "dataSourceName": "jsonData", + "variableId": "jsonTable", + "editable": true, + "tabulatorOptions": { + "columns": [ + { + "title": "LineTemplate", + "field": "lineTemplate", + "editor": "list", + "editorParams": { + "values": [ + "node", + "edge", + "labeledEdge" + ] + } + }, + { + "title": "ID", + "field": "id", + "editor": "input" + }, + { + "title": "Label", + "field": "label", + "editor": "input" + }, + { + "title": "From", + "field": "from", + "editor": "input" + }, + { + "title": "To", + "field": "to", + "editor": "input" + }, + { + "title": "Text", + "field": "text", + "editor": "input" + } + ], + "layout": "fitColumns", + "maxHeight": "150px" + } + }, + { + "type": "mermaid", + "template": { + "dataSourceName": "jsonTable", + "header": "flowchart TD", + "lineTemplates": { + "node": "{{id}}[{{label}}]", + "edge": "{{from}} --> {{to}}", + "labeledEdge": "{{from}} -->|{{label}}| {{to}}" + } + }, + "variableId": "flowchartOutput" + }, + "### Generated Mermaid Source:", + "```\n{{flowchartOutput}}\n```" + ] + }, + { + "groupId": "network", + "elements": [ + "## More Complex Example", + "Network diagram with servers and connections:", + { + "type": "table", + "dataSourceName": "networkData", + "variableId": "networkTable", + "editable": true, + "tabulatorOptions": { + "columns": [ + { + "title": "LineTemplate", + "field": "lineTemplate", + "editor": "list", + "editorParams": { + "values": [ + "server", + "connection", + "secureConnection", + "subgraph", + "end" + ] + } + }, + { + "title": "ID", + "field": "id", + "editor": "input" + }, + { + "title": "Name", + "field": "name", + "editor": "input" + }, + { + "title": "IP", + "field": "ip", + "editor": "input" + }, + { + "title": "From", + "field": "from", + "editor": "input" + }, + { + "title": "To", + "field": "to", + "editor": "input" + } + ], + "layout": "fitColumns", + "maxHeight": "200px" + } + }, + { + "type": "mermaid", + "template": { + "dataSourceName": "networkTable", + "header": "graph LR", + "lineTemplates": { + "server": "{{id}}[{{name}}
{{ip}}]", + "connection": "{{from}} --- {{to}}", + "secureConnection": "{{from}} -.->|SSL| {{to}}", + "subgraph": "subgraph {{name}}", + "end": "end" + } + }, + "variableId": "networkOutput" + }, + "### Generated Network Diagram Source:", + { + "type": "textbox", + "variableId": "networkOutput", + "multiline": true + }, + "## String Input Mode", + "This example shows how to consume the generated Mermaid text from above and render it directly:", + { + "type": "mermaid", + "variableId": "networkOutput" + }, + "This demonstrates the flexible input capability - the same `dataSourceName` property can handle string or array input." + ] + } + ] +} \ No newline at end of file diff --git a/docs/assets/examples/json/mermaid-org chart.idoc.json b/docs/assets/examples/json/mermaid-org chart.idoc.json new file mode 100644 index 00000000..0a9335f9 --- /dev/null +++ b/docs/assets/examples/json/mermaid-org chart.idoc.json @@ -0,0 +1,198 @@ +{ + "$schema": "https://microsoft.github.io/chartifact/schema/idoc_v1.json", + "title": "Org Chart Example (Mermaid)", + "dataLoaders": [ + { + "dataSourceName": "orgChartData", + "type": "inline", + "format": "json", + "content": [ + { + "lineTemplate": "person", + "id": "CEO", + "label": "CEO
Alice Smith" + }, + { + "lineTemplate": "person", + "id": "CTO", + "label": "CTO
Bob Lee" + }, + { + "lineTemplate": "person", + "id": "CFO", + "label": "CFO
Carol Jones" + }, + { + "lineTemplate": "person", + "id": "COO", + "label": "COO
David Kim" + }, + { + "lineTemplate": "person", + "id": "ENG1", + "label": "Lead Engineer
Eve Tran" + }, + { + "lineTemplate": "person", + "id": "ENG2", + "label": "Engineer
Frank Wu" + }, + { + "lineTemplate": "person", + "id": "ENG3", + "label": "Engineer
Grace Lin" + }, + { + "lineTemplate": "person", + "id": "ACC1", + "label": "Accountant
Henry Patel" + }, + { + "lineTemplate": "person", + "id": "ACC2", + "label": "Accountant
Ivy Chen" + }, + { + "lineTemplate": "person", + "id": "OPS1", + "label": "Operations Mgr
Jack Brown" + }, + { + "lineTemplate": "person", + "id": "OPS2", + "label": "Ops Specialist
Kim Lee" + }, + { + "lineTemplate": "orgEdge", + "from": "CEO", + "to": "CTO" + }, + { + "lineTemplate": "orgEdge", + "from": "CEO", + "to": "CFO" + }, + { + "lineTemplate": "orgEdge", + "from": "CEO", + "to": "COO" + }, + { + "lineTemplate": "orgEdge", + "from": "CTO", + "to": "ENG1" + }, + { + "lineTemplate": "orgEdge", + "from": "CTO", + "to": "ENG2" + }, + { + "lineTemplate": "orgEdge", + "from": "CTO", + "to": "ENG3" + }, + { + "lineTemplate": "orgEdge", + "from": "CFO", + "to": "ACC1" + }, + { + "lineTemplate": "orgEdge", + "from": "CFO", + "to": "ACC2" + }, + { + "lineTemplate": "orgEdge", + "from": "COO", + "to": "OPS1" + }, + { + "lineTemplate": "orgEdge", + "from": "COO", + "to": "OPS2" + } + ] + } + ], + "variables": [ + { + "variableId": "direction", + "type": "string", + "initialValue": "TD" + }, + { + "variableId": "orgChartOutput", + "type": "string", + "initialValue": "" + } + ], + "groups": [ + { + "groupId": "main", + "elements": [ + "# Mermaid Plugin Example - Org Chart", + "Template-based org chart generation:", + { + "type": "mermaid", + "template": { + "dataSourceName": "orgChartDataTable", + "header": "graph {{direction}}", + "lineTemplates": { + "person": "{{id}}[{{label}}]", + "orgEdge": "{{from}} --> {{to}}" + } + }, + "variableId": "orgChartOutput" + }, + { + "type": "dropdown", + "variableId": "direction", + "options": [ + "TD", + "LR" + ] + }, + "## JSON Data", + "Load data from a static JSON array.", + { + "type": "table", + "dataSourceName": "orgChartData", + "variableId": "orgChartDataTable", + "editable": true, + "tabulatorOptions": { + "columns": [ + { + "title": "LineTemplate", + "field": "lineTemplate", + "editor": "input" + }, + { + "title": "ID", + "field": "id", + "editor": "input" + }, + { + "title": "Label", + "field": "label", + "editor": "input" + }, + { + "title": "From", + "field": "from", + "editor": "input" + }, + { + "title": "To", + "field": "to", + "editor": "input" + } + ], + "layout": "fitColumns", + "maxHeight": "300px" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/assets/examples/markdown/features/9.mermaid.idoc.md b/docs/assets/examples/markdown/features/9.mermaid.idoc.md new file mode 100644 index 00000000..91bd72ae --- /dev/null +++ b/docs/assets/examples/markdown/features/9.mermaid.idoc.md @@ -0,0 +1,302 @@ +```json vega +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "description": "This is the central brain of the page", + "signals": [ + { + "name": "jsonTable", + "update": "data('jsonTable')" + }, + { + "name": "networkTable", + "update": "data('networkTable')" + }, + { + "name": "flowchartOutput", + "value": "" + }, + { + "name": "networkOutput", + "value": "" + }, + { + "name": "jsonData", + "update": "data('jsonData')" + }, + { + "name": "networkData", + "update": "data('networkData')" + } + ], + "data": [ + { + "name": "networkData", + "values": [ + { + "lineTemplate": "subgraph", + "name": "Production" + }, + { + "lineTemplate": "server", + "id": "web1", + "name": "Web Server", + "ip": "10.0.1.10" + }, + { + "lineTemplate": "server", + "id": "db1", + "name": "Database", + "ip": "10.0.1.20" + }, + { + "lineTemplate": "end" + }, + { + "lineTemplate": "subgraph", + "name": "Development" + }, + { + "lineTemplate": "server", + "id": "dev1", + "name": "Dev Server", + "ip": "10.0.2.10" + }, + { + "lineTemplate": "end" + }, + { + "lineTemplate": "secureConnection", + "from": "web1", + "to": "db1" + }, + { + "lineTemplate": "connection", + "from": "dev1", + "to": "web1" + } + ] + }, + { + "name": "jsonData", + "values": [ + { + "lineTemplate": "node", + "id": "A", + "label": "Start" + }, + { + "lineTemplate": "node", + "id": "B", + "label": "Middle" + }, + { + "lineTemplate": "node", + "id": "C", + "label": "End" + }, + { + "lineTemplate": "labeledEdge", + "from": "A", + "to": "B", + "label": "Next" + }, + { + "lineTemplate": "edge", + "from": "B", + "to": "C" + } + ] + }, + { + "name": "networkTable", + "values": [] + }, + { + "name": "jsonTable", + "values": [] + } + ] +} +``` + +# Mermaid Plugin Examples + +This page demonstrates the mermaid plugin in Chartifact, including raw text, data-driven, and dynamic modes. + +## Raw Text Mode + +Simple flowchart using raw mermaid syntax: + +```mermaid +flowchart TD + A[Start] --> B{Is it?} + B -->|Yes| C[OK] + B -->|No| D[End] + C --> D +``` + +## Data-Driven Mode + +Template-based diagram generation: + +```json tabulator +{ + "dataSourceName": "jsonData", + "variableId": "jsonTable", + "tabulatorOptions": { + "columns": [ + { + "title": "LineTemplate", + "field": "lineTemplate", + "editor": "list", + "editorParams": { + "values": [ + "node", + "edge", + "labeledEdge" + ] + } + }, + { + "title": "ID", + "field": "id", + "editor": "input" + }, + { + "title": "Label", + "field": "label", + "editor": "input" + }, + { + "title": "From", + "field": "from", + "editor": "input" + }, + { + "title": "To", + "field": "to", + "editor": "input" + }, + { + "title": "Text", + "field": "text", + "editor": "input" + } + ], + "layout": "fitColumns", + "maxHeight": "150px" + }, + "editable": true +} +``` + +```json mermaid +{ + "template": { + "dataSourceName": "jsonTable", + "header": "flowchart TD", + "lineTemplates": { + "node": "{{id}}[{{label}}]", + "edge": "{{from}} --> {{to}}", + "labeledEdge": "{{from}} -->|{{label}}| {{to}}" + } + }, + "variableId": "flowchartOutput" +} +``` + +### Generated Mermaid Source: + +``` +{{flowchartOutput}} +``` + +## More Complex Example + +Network diagram with servers and connections: + +```json tabulator +{ + "dataSourceName": "networkData", + "variableId": "networkTable", + "tabulatorOptions": { + "columns": [ + { + "title": "LineTemplate", + "field": "lineTemplate", + "editor": "list", + "editorParams": { + "values": [ + "server", + "connection", + "secureConnection", + "subgraph", + "end" + ] + } + }, + { + "title": "ID", + "field": "id", + "editor": "input" + }, + { + "title": "Name", + "field": "name", + "editor": "input" + }, + { + "title": "IP", + "field": "ip", + "editor": "input" + }, + { + "title": "From", + "field": "from", + "editor": "input" + }, + { + "title": "To", + "field": "to", + "editor": "input" + } + ], + "layout": "fitColumns", + "maxHeight": "200px" + }, + "editable": true +} +``` + +```json mermaid +{ + "template": { + "dataSourceName": "networkTable", + "header": "graph LR", + "lineTemplates": { + "server": "{{id}}[{{name}}
{{ip}}]", + "connection": "{{from}} --- {{to}}", + "secureConnection": "{{from}} -.->|SSL| {{to}}", + "subgraph": "subgraph {{name}}", + "end": "end" + } + }, + "variableId": "networkOutput" +} +``` + +### Generated Network Diagram Source: + +```json textbox +{"variableId":"networkOutput","value":"","multiline":true} +``` + +## String Input Mode + +This example shows how to consume the generated Mermaid text from above and render it directly: + +```json mermaid +{"variableId":"networkOutput"} +``` + +This demonstrates the flexible input capability - the same `dataSourceName` property can handle string or array input. \ No newline at end of file diff --git a/docs/assets/examples/markdown/mermaid-org chart.idoc.md b/docs/assets/examples/markdown/mermaid-org chart.idoc.md new file mode 100644 index 00000000..4ff73510 --- /dev/null +++ b/docs/assets/examples/markdown/mermaid-org chart.idoc.md @@ -0,0 +1,212 @@ +```json vega +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "description": "This is the central brain of the page", + "signals": [ + { + "name": "orgChartDataTable", + "update": "data('orgChartDataTable')" + }, + { + "name": "direction", + "value": "TD" + }, + { + "name": "orgChartOutput", + "value": "" + }, + { + "name": "orgChartData", + "update": "data('orgChartData')" + } + ], + "data": [ + { + "name": "orgChartData", + "values": [ + { + "lineTemplate": "person", + "id": "CEO", + "label": "CEO
Alice Smith" + }, + { + "lineTemplate": "person", + "id": "CTO", + "label": "CTO
Bob Lee" + }, + { + "lineTemplate": "person", + "id": "CFO", + "label": "CFO
Carol Jones" + }, + { + "lineTemplate": "person", + "id": "COO", + "label": "COO
David Kim" + }, + { + "lineTemplate": "person", + "id": "ENG1", + "label": "Lead Engineer
Eve Tran" + }, + { + "lineTemplate": "person", + "id": "ENG2", + "label": "Engineer
Frank Wu" + }, + { + "lineTemplate": "person", + "id": "ENG3", + "label": "Engineer
Grace Lin" + }, + { + "lineTemplate": "person", + "id": "ACC1", + "label": "Accountant
Henry Patel" + }, + { + "lineTemplate": "person", + "id": "ACC2", + "label": "Accountant
Ivy Chen" + }, + { + "lineTemplate": "person", + "id": "OPS1", + "label": "Operations Mgr
Jack Brown" + }, + { + "lineTemplate": "person", + "id": "OPS2", + "label": "Ops Specialist
Kim Lee" + }, + { + "lineTemplate": "orgEdge", + "from": "CEO", + "to": "CTO" + }, + { + "lineTemplate": "orgEdge", + "from": "CEO", + "to": "CFO" + }, + { + "lineTemplate": "orgEdge", + "from": "CEO", + "to": "COO" + }, + { + "lineTemplate": "orgEdge", + "from": "CTO", + "to": "ENG1" + }, + { + "lineTemplate": "orgEdge", + "from": "CTO", + "to": "ENG2" + }, + { + "lineTemplate": "orgEdge", + "from": "CTO", + "to": "ENG3" + }, + { + "lineTemplate": "orgEdge", + "from": "CFO", + "to": "ACC1" + }, + { + "lineTemplate": "orgEdge", + "from": "CFO", + "to": "ACC2" + }, + { + "lineTemplate": "orgEdge", + "from": "COO", + "to": "OPS1" + }, + { + "lineTemplate": "orgEdge", + "from": "COO", + "to": "OPS2" + } + ] + }, + { + "name": "orgChartDataTable", + "values": [] + } + ] +} +``` + +# Mermaid Plugin Example - Org Chart + +Template-based org chart generation: + +```json mermaid +{ + "template": { + "dataSourceName": "orgChartDataTable", + "header": "graph {{direction}}", + "lineTemplates": { + "person": "{{id}}[{{label}}]", + "orgEdge": "{{from}} --> {{to}}" + } + }, + "variableId": "orgChartOutput" +} +``` + +```json dropdown +{ + "variableId": "direction", + "value": "TD", + "options": [ + "TD", + "LR" + ] +} +``` + +## JSON Data + +Load data from a static JSON array. + +```json tabulator +{ + "dataSourceName": "orgChartData", + "variableId": "orgChartDataTable", + "tabulatorOptions": { + "columns": [ + { + "title": "LineTemplate", + "field": "lineTemplate", + "editor": "input" + }, + { + "title": "ID", + "field": "id", + "editor": "input" + }, + { + "title": "Label", + "field": "label", + "editor": "input" + }, + { + "title": "From", + "field": "from", + "editor": "input" + }, + { + "title": "To", + "field": "to", + "editor": "input" + } + ], + "layout": "fitColumns", + "maxHeight": "300px" + }, + "editable": true +} +``` \ No newline at end of file diff --git a/docs/dist/v1/chartifact-reset.css b/docs/dist/v1/chartifact-reset.css index bdc6be51..7ab6f4d7 100644 --- a/docs/dist/v1/chartifact-reset.css +++ b/docs/dist/v1/chartifact-reset.css @@ -61,3 +61,7 @@ ul img { .chartifact-plugin-vega form>:first-child { margin-top: 4px; } + +.chartifact-plugin-mermaid { + text-align: center; +} diff --git a/docs/dist/v1/chartifact.compiler.umd.js b/docs/dist/v1/chartifact.compiler.umd.js index 328ba037..4daf2da7 100644 --- a/docs/dist/v1/chartifact.compiler.umd.js +++ b/docs/dist/v1/chartifact.compiler.umd.js @@ -330,7 +330,11 @@ ${content} ${content} :::`; } - function targetMarkdown(page) { + const defaultOptions = { + extraNewlineCount: 1 + }; + function targetMarkdown(page, options) { + const finalOptions = { ...defaultOptions, ...options }; const mdSections = []; const dataLoaders = page.dataLoaders || []; const variables = page.variables || []; @@ -393,7 +397,8 @@ ${content} mdSections.unshift(tickWrap("#", JSON.stringify(page.notes, null, defaultJsonIndent))); } } - const markdown = mdSections.join("\n\n"); + const newLines = "\n".repeat(1 + finalOptions.extraNewlineCount); + const markdown = mdSections.join(newLines); return markdown; } function dataLoaderMarkdown(dataSources, variables, tableElements) { @@ -483,6 +488,26 @@ ${content} addSpec("image", imageSpec); break; } + case "mermaid": { + const { diagramText, template, variableId } = element; + if (diagramText) { + mdElements.push(tickWrap("mermaid", diagramText)); + } else if (template) { + const mermaidSpec = { + template + }; + if (variableId) { + mermaidSpec.variableId = variableId; + } + addSpec("mermaid", mermaidSpec); + } else if (variableId) { + const mermaidSpec = { + variableId + }; + addSpec("mermaid", mermaidSpec, false); + } + break; + } case "presets": { const { presets } = element; const presetsSpec = presets; diff --git a/docs/dist/v1/chartifact.editor.umd.js b/docs/dist/v1/chartifact.editor.umd.js index 1dd63e3f..11c3041c 100644 --- a/docs/dist/v1/chartifact.editor.umd.js +++ b/docs/dist/v1/chartifact.editor.umd.js @@ -332,7 +332,11 @@ ${content} ${content} :::`; } - function targetMarkdown(page) { + const defaultOptions = { + extraNewlineCount: 1 + }; + function targetMarkdown(page, options) { + const finalOptions = { ...defaultOptions, ...options }; const mdSections = []; const dataLoaders = page.dataLoaders || []; const variables = page.variables || []; @@ -391,7 +395,8 @@ ${content} mdSections.unshift(tickWrap("#", JSON.stringify(page.notes, null, defaultJsonIndent))); } } - const markdown = mdSections.join("\n\n"); + const newLines = "\n".repeat(1 + finalOptions.extraNewlineCount); + const markdown = mdSections.join(newLines); return markdown; } function dataLoaderMarkdown(dataSources, variables, tableElements) { @@ -481,6 +486,26 @@ ${content} addSpec("image", imageSpec); break; } + case "mermaid": { + const { diagramText, template, variableId } = element; + if (diagramText) { + mdElements.push(tickWrap("mermaid", diagramText)); + } else if (template) { + const mermaidSpec = { + template + }; + if (variableId) { + mermaidSpec.variableId = variableId; + } + addSpec("mermaid", mermaidSpec); + } else if (variableId) { + const mermaidSpec = { + variableId + }; + addSpec("mermaid", mermaidSpec, false); + } + break; + } case "presets": { const { presets } = element; const presetsSpec = presets; diff --git a/docs/dist/v1/chartifact.host.umd.js b/docs/dist/v1/chartifact.host.umd.js index 91e162e6..a6d90fa5 100644 --- a/docs/dist/v1/chartifact.host.umd.js +++ b/docs/dist/v1/chartifact.host.umd.js @@ -332,7 +332,11 @@ ${content} ${content} :::`; } - function targetMarkdown(page) { + const defaultOptions$1 = { + extraNewlineCount: 1 + }; + function targetMarkdown(page, options) { + const finalOptions = { ...defaultOptions$1, ...options }; const mdSections = []; const dataLoaders = page.dataLoaders || []; const variables = page.variables || []; @@ -391,7 +395,8 @@ ${content} mdSections.unshift(tickWrap("#", JSON.stringify(page.notes, null, defaultJsonIndent))); } } - const markdown = mdSections.join("\n\n"); + const newLines = "\n".repeat(1 + finalOptions.extraNewlineCount); + const markdown = mdSections.join(newLines); return markdown; } function dataLoaderMarkdown(dataSources, variables, tableElements) { @@ -481,6 +486,26 @@ ${content} addSpec("image", imageSpec); break; } + case "mermaid": { + const { diagramText, template, variableId } = element; + if (diagramText) { + mdElements.push(tickWrap("mermaid", diagramText)); + } else if (template) { + const mermaidSpec = { + template + }; + if (variableId) { + mermaidSpec.variableId = variableId; + } + addSpec("mermaid", mermaidSpec); + } else if (variableId) { + const mermaidSpec = { + variableId + }; + addSpec("mermaid", mermaidSpec, false); + } + break; + } case "presets": { const { presets } = element; const presetsSpec = presets; @@ -1411,7 +1436,7 @@ ${guardedJs} ${message} ${details}`; - this.render("Error", markdown, void 0, true); + this.renderMarkdown(markdown); } else { this.previewDiv.innerHTML = ""; const h1 = document.createElement("h1"); diff --git a/docs/dist/v1/chartifact.markdown.umd.js b/docs/dist/v1/chartifact.markdown.umd.js index 0831c7de..292c2f2a 100644 --- a/docs/dist/v1/chartifact.markdown.umd.js +++ b/docs/dist/v1/chartifact.markdown.umd.js @@ -434,65 +434,61 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy }; p.block.ruler.before("fence", `container_${n}`, I2, { alt: ["paragraph", "reference", "blockquote", "list"] }), p.renderer.rules[`container_${n}_open`] = g2, p.renderer.rules[`container_${n}_close`] = C2; }; - const plugins = []; - function registerMarkdownPlugin(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); + class DynamicUrl { + constructor(templateUrl, onChange) { + __publicField(this, "signals"); + __publicField(this, "tokens"); + __publicField(this, "lastUrl"); + this.templateUrl = templateUrl; + this.onChange = onChange; + this.signals = {}; + this.tokens = tokenizeTemplate(templateUrl); + const signalNames = this.tokens.filter((token) => token.type === "variable").map((token) => token.name); + if (signalNames.length === 0) { + onChange(templateUrl); + this.lastUrl = templateUrl; + return; } + signalNames.forEach((signalName) => { + this.signals[signalName] = void 0; + }); } - if (plugin.hydratesBefore) { - const targetIndex = plugins.findIndex((p) => p.name === plugin.hydratesBefore); - if (targetIndex !== -1) { - insertIndex = targetIndex; + makeUrl() { + const signalNames = Object.keys(this.signals); + if (signalNames.length === 0) { + return this.templateUrl; } - } - insertIndex = Math.max(insertIndex, minIndex); - plugins.splice(insertIndex, 0, plugin); - return "register"; - } - function create() { - var _a; - const md = new markdownit(); - for (const plugin of plugins) { - (_a = plugin.initializePlugin) == null ? void 0 : _a.call(plugin, md); - } - md.use(G); - const containerOptions = { name: defaultCommonOptions.groupClassName }; - md.use(R, containerOptions); - const originalFence = md.renderer.rules.fence; - md.renderer.rules.fence = function(tokens, idx, options, env, slf) { - const token = tokens[idx]; - const info = token.info.trim(); - const findPlugin = (pluginName2) => { - const plugin = plugins.find((p) => p.name === pluginName2); - if (plugin && plugin.fence) { - return plugin.fence(token, idx); + if (this.tokens.length === 1 && this.tokens[0].type === "variable") { + return this.signals[this.tokens[0].name] || ""; + } + const urlParts = []; + this.tokens.forEach((token) => { + if (token.type === "literal") { + urlParts.push(token.value); + } else if (token.type === "variable") { + const signalValue = this.signals[token.name]; + if (signalValue !== void 0) { + urlParts.push(encodeURIComponent(signalValue)); + } } - }; - if (info.startsWith("#")) { - return findPlugin("#"); - } else { - const directPlugin = findPlugin(info); - if (directPlugin) { - return directPlugin; - } else if (info.startsWith("json ")) { - const jsonPluginName = info.slice(5).trim(); - const jsonPlugin = findPlugin(jsonPluginName); - if (jsonPlugin) { - return jsonPlugin; + }); + return urlParts.join(""); + } + receiveBatch(batch) { + for (const [signalName, batchItem] of Object.entries(batch)) { + if (signalName in this.signals) { + if (batchItem.isData || batchItem.value === void 0) { + continue; } + this.signals[signalName] = batchItem.value.toString(); } } - if (originalFence) { - return originalFence(tokens, idx, options, env, slf); - } else { - return ""; + const newUrl = this.makeUrl(); + if (newUrl !== this.lastUrl) { + this.onChange(newUrl); + this.lastUrl = newUrl; } - }; - return md; + } } function getJsonScriptTag(container, errorHandler) { const scriptTag = container.previousElementSibling; @@ -563,36 +559,501 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy 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); + 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; + } + const ImageOpacity = { + full: "1", + loading: "0.1", + error: "0.5" + }; + const pluginName$d = "image"; + const className$b = pluginClassName(pluginName$d); + const imagePlugin = { + ...flaggableJsonPlugin(pluginName$d, className$b), + hydrateComponent: async (renderer, errorHandler, specs) => { + const imageInstances = []; + 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; + container.innerHTML = createImageContainerTemplate("", spec.alt, spec.url, index2, errorHandler); + const { img, spinner, retryBtn, dynamicUrl } = createImageLoadingLogic( + container, + null, + (error) => { + errorHandler(error, pluginName$d, index2, "load", container, img.src); + } + ); + const imageInstance = { + id: `${pluginName$d}-${index2}`, + spec, + img: null, + // Will be set below + spinner: null, + // Will be set below + dynamicUrl + }; + imageInstance.img = img; + imageInstance.spinner = spinner; + if (spec.alt) img.alt = spec.alt; + if (spec.width) img.width = spec.width; + if (spec.height) img.height = spec.height; + imageInstances.push(imageInstance); + } + const instances = imageInstances.map((imageInstance, index2) => { + const { img, spinner, id, dynamicUrl } = imageInstance; + const signalNames = Object.keys((dynamicUrl == null ? void 0 : dynamicUrl.signals) || {}); + return { + id, + initialSignals: Array.from(signalNames).map((name) => ({ + name, + value: null, + priority: -1, + isData: false + })), + destroy: () => { + if (img) { + img.remove(); + } + if (spinner) { + spinner.remove(); + } + }, + receiveBatch: async (batch, from) => { + dynamicUrl == null ? void 0 : dynamicUrl.receiveBatch(batch); + } + }; + }); + return instances; + } + }; + const imgSpinner = ` + + + + + +`; + function createImageContainerTemplate(clasName, alt, src, instanceIndex, errorHandler) { + const tempImg = document.createElement("img"); + if (src.includes("{{")) { + tempImg.setAttribute("src", "data:,"); + tempImg.setAttribute("data-dynamic-url", src); + } else { + if (isSafeImageUrl(src)) { + tempImg.setAttribute("src", src); + } else { + errorHandler(new Error(`Unsafe image URL: ${src}`), pluginName$d, instanceIndex, "load", null, src); + } + } + tempImg.setAttribute("alt", alt); + const imgHtml = tempImg.outerHTML; + return ` + + ${imgHtml} + + `; + } + function createImageLoadingLogic(container, onSuccess, onError) { + container.style.position = "relative"; + const img = container.querySelector("img"); + const spinner = container.querySelector(".image-spinner"); + const retryBtn = container.querySelector(".image-retry"); + const dataDynamicUrl = img.getAttribute("data-dynamic-url"); + img.onload = () => { + spinner.style.display = "none"; + img.style.opacity = ImageOpacity.full; + img.style.display = ""; + retryBtn.style.display = "none"; + img.setAttribute("hasImage", "true"); + }; + img.onerror = () => { + spinner.style.display = "none"; + img.style.opacity = ImageOpacity.error; + img.style.display = "none"; + retryBtn.style.display = ""; + retryBtn.disabled = false; + img.setAttribute("hasImage", "false"); + onError == null ? void 0 : onError(new Error("Image failed to load")); + }; + retryBtn.onclick = () => { + retryBtn.disabled = true; + spinner.style.display = ""; + img.style.opacity = ImageOpacity.loading; + img.style.display = img.getAttribute("hasImage") ? "" : "none"; + const src = img.src; + const onload = img.onload; + const onerror = img.onerror; + img.src = "data:,"; + img.onload = null; + img.onerror = null; + setTimeout(() => { + img.onload = onload; + img.onerror = onerror; + img.src = src; + }, 100); + }; + const result = { img, spinner, retryBtn }; + if (dataDynamicUrl) { + const dynamicUrl = new DynamicUrl(dataDynamicUrl, (src) => { + if (isSafeImageUrl(src)) { + spinner.style.display = ""; + img.src = src; + img.style.opacity = ImageOpacity.loading; + } else { + img.src = ""; + spinner.style.display = "none"; + img.style.opacity = ImageOpacity.full; + } + }); + result.dynamicUrl = dynamicUrl; + } + return result; + } + function isSafeImageUrl(url) { + try { + if (url.startsWith("data:image/")) { + const safeMimeTypes = [ + "data:image/png", + "data:image/jpeg", + "data:image/gif", + "data:image/webp", + "data:image/bmp", + "data:image/x-icon" + ]; + for (const mime of safeMimeTypes) { + if (url.startsWith(mime)) { + return true; + } + } + return false; + } + const parsed = new URL(url, window.location.origin); + return parsed.protocol === "http:" || parsed.protocol === "https:"; + } catch { + return false; + } + } + function decorateDynamicUrl(tokens, idx, attrName, elementType) { + const token = tokens[idx]; + const attrValue = token.attrGet(attrName); + if (attrValue && attrValue.includes("%7B%7B")) { + if (!token.attrs) { + token.attrs = []; + } + token.attrSet("dynamic-url", decodeURIComponent(attrValue)); + token.attrSet(attrName, ""); + } + return token; + } + function decorateFenceWithPlaceholders(tokens, idx) { + const token = tokens[idx]; + const content = token.content; + if (content && content.includes("{{")) { + const templateTokens = tokenizeTemplate(content); + const variableTokens = templateTokens.filter((t) => t.type === "variable"); + if (variableTokens.length > 0) { + const templateContent = encodeURIComponent(content); + const placeholderData = variableTokens.map((t) => `data-placeholder-${t.name.toLowerCase()}="true"`).join(" "); + return `
`; + } + } + return null; + } + const pluginName$c = "placeholders"; + const imageClassName = pluginClassName(pluginName$c + "_image"); + const placeholdersPlugin = { + name: pluginName$c, + initializePlugin: async (md) => { + md.use(function(md2) { + md2.inline.ruler.after("emphasis", "dynamic_placeholder", function(state, silent) { + let token; + const max = state.posMax; + const start = state.pos; + if (state.src.charCodeAt(start) !== 123 || state.src.charCodeAt(start + 1) !== 123) { + return false; + } + for (let pos = start + 2; pos < max; pos++) { + if (state.src.charCodeAt(pos) === 125 && state.src.charCodeAt(pos + 1) === 125) { + if (!silent) { + state.pos = start + 2; + state.posMax = pos; + token = state.push("dynamic_placeholder", "", 0); + token.markup = state.src.slice(start, pos + 2); + token.content = state.src.slice(state.pos, state.posMax); + state.pos = pos + 2; + state.posMax = max; + } + return true; + } + } + return false; + }); + md2.renderer.rules["dynamic_placeholder"] = function(tokens, idx) { + const key = tokens[idx].content.trim(); + return `{${key}}`; + }; + }); + md.renderer.rules["link_open"] = function(tokens, idx, options, env, slf) { + decorateDynamicUrl(tokens, idx, "href"); + return slf.renderToken(tokens, idx, options); + }; + md.renderer.rules["image"] = function(tokens, idx, options, env, slf) { + const alt = tokens[idx].attrGet("alt"); + const src = tokens[idx].attrGet("src"); + let error; + const html = createImageContainerTemplate(imageClassName, alt, decodeURIComponent(src), idx, (e, pluginName2, instanceIndex, phase, container, detail) => { + error = sanitizeHtmlComment(`Error in plugin ${pluginName2} instance ${instanceIndex} phase ${phase}: ${e.message} ${detail}`); + }); + return error || html; + }; + }, + hydrateComponent: async (renderer, errorHandler) => { + const dynamicUrlMap = /* @__PURE__ */ new WeakMap(); + const templateHandlerMap = /* @__PURE__ */ new WeakMap(); + const placeholders = renderer.element.querySelectorAll(".dynamic-placeholder"); + const dynamicUrls = renderer.element.querySelectorAll("[dynamic-url]"); + const dynamicImages = renderer.element.querySelectorAll(`.${imageClassName}`); + const codeBlocksWithPlaceholders = renderer.element.querySelectorAll(".has-placeholders[data-template-content]"); + const elementsByKeys = /* @__PURE__ */ new Map(); + for (const placeholder of Array.from(placeholders)) { + const key = placeholder.getAttribute("data-key"); + if (!key) { + continue; + } + if (elementsByKeys.has(key)) { + elementsByKeys.get(key).push(placeholder); + } else { + elementsByKeys.set(key, [placeholder]); + } + } + for (const element of Array.from(codeBlocksWithPlaceholders)) { + const templateContent = element.getAttribute("data-template-content"); + if (!templateContent) { + continue; + } + const templateText = decodeURIComponent(templateContent); + const tokens = tokenizeTemplate(templateText); + const variableTokens = tokens.filter((token) => token.type === "variable"); + const templateHandler = { + signals: {}, + update: () => { + let content = ""; + for (const token of tokens) { + if (token.type === "literal") { + content += token.value; + } else if (token.type === "variable") { + content += templateHandler.signals[token.name] || ""; + } + } + element.innerHTML = `${content}`; + } + }; + variableTokens.forEach((token) => { + templateHandler.signals[token.name] = ""; + }); + templateHandlerMap.set(element, templateHandler); + variableTokens.forEach((token) => { + const key = token.name; + if (elementsByKeys.has(key)) { + elementsByKeys.get(key).push(element); + } else { + elementsByKeys.set(key, [element]); + } + }); + } + for (const element of Array.from(dynamicUrls)) { + const templateUrl = element.getAttribute("dynamic-url"); + if (!templateUrl) { + continue; + } + if (element.tagName === "A") { + const dynamicUrl = new DynamicUrl(templateUrl, (url) => { + element.setAttribute("href", url); + }); + dynamicUrlMap.set(element, dynamicUrl); + for (const key of Object.keys(dynamicUrl.signals)) { + if (elementsByKeys.has(key)) { + elementsByKeys.get(key).push(element); + } else { + elementsByKeys.set(key, [element]); + } + } + } + } + for (const element of Array.from(dynamicImages)) { + const { dynamicUrl, img } = createImageLoadingLogic(element, null, (error) => { + const index2 = -1; + errorHandler(error, pluginName$c, index2, "load", element, img.src); + }); + if (!dynamicUrl) { + continue; + } + dynamicUrlMap.set(element, dynamicUrl); + for (const key of Object.keys(dynamicUrl.signals)) { + if (elementsByKeys.has(key)) { + elementsByKeys.get(key).push(element); + } else { + elementsByKeys.set(key, [element]); + } + } + } + const initialSignals = Array.from(elementsByKeys.keys()).map((name) => { + const prioritizedSignal = { + name, + value: null, + priority: -1, + isData: false + }; + return prioritizedSignal; + }); + const instances = [ + { + id: pluginName$c, + initialSignals, + receiveBatch: async (batch) => { + var _a, _b; + for (const key of Object.keys(batch)) { + const elements = elementsByKeys.get(key) || []; + for (const element of elements) { + if (element.classList.contains("dynamic-placeholder")) { + const markdownContent = ((_a = batch[key].value) == null ? void 0 : _a.toString()) || ""; + const parsedMarkdown = isMarkdownInline(markdownContent) ? renderer.md.renderInline(markdownContent) : renderer.md.render(markdownContent); + element.innerHTML = parsedMarkdown; + } else if (element.hasAttribute("dynamic-url")) { + const dynamicUrl = dynamicUrlMap.get(element); + if (dynamicUrl) { + dynamicUrl.receiveBatch(batch); + } + } else if (element.classList.contains(imageClassName)) { + const dynamicUrl = dynamicUrlMap.get(element); + if (dynamicUrl) { + dynamicUrl.receiveBatch(batch); + } + } else if (element.hasAttribute("data-template-content")) { + const templateHandler = templateHandlerMap.get(element); + if (templateHandler && templateHandler.signals) { + templateHandler.signals[key] = ((_b = batch[key].value) == null ? void 0 : _b.toString()) || ""; + templateHandler.update(); + } + } + } + } + } + } + ]; + return instances; + } + }; + function isMarkdownInline(markdown) { + if (!markdown.includes("\n")) { + return true; + } + const blockElements = ["#", "-", "*", ">", "```", "~~~"]; + for (const element of blockElements) { + if (markdown.trim().startsWith(element)) { + return false; + } + } + return true; + } + const plugins = []; + function registerMarkdownPlugin(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() { + var _a; + const md = new markdownit(); + for (const plugin of plugins) { + (_a = plugin.initializePlugin) == null ? void 0 : _a.call(plugin, md); + } + md.use(G); + const containerOptions = { name: defaultCommonOptions.groupClassName }; + md.use(R, containerOptions); + const originalFence = md.renderer.rules.fence; + md.renderer.rules.fence = function(tokens, idx, options, env, slf) { + const token = tokens[idx]; + const info = token.info.trim(); + const findPlugin = (pluginName2) => { + const plugin = plugins.find((p) => p.name === pluginName2); + if (plugin && plugin.fence) { + return plugin.fence(token, idx); + } + }; + if (info.startsWith("#")) { + return findPlugin("#"); + } else { + const directPlugin = findPlugin(info); + if (directPlugin) { + return directPlugin; + } else if (info.startsWith("json ")) { + const jsonPluginName = info.slice(5).trim(); + const jsonPlugin = findPlugin(jsonPluginName); + if (jsonPlugin) { + return jsonPlugin; + } + } + } + if (originalFence) { + const originalResult = originalFence(tokens, idx, options, env, slf); + if (token.content && token.content.includes("{{")) { + return decorateFenceWithPlaceholders([token], 0); } - return flagged; + return originalResult; + } else { + return ""; } }; - return plugin; + return md; } - const pluginName$c = "checkbox"; - const className$a = pluginClassName(pluginName$c); + const pluginName$b = "checkbox"; + const className$a = pluginClassName(pluginName$b); const checkboxPlugin = { - ...flaggableJsonPlugin(pluginName$c, className$a), + ...flaggableJsonPlugin(pluginName$b, className$a), hydrateComponent: async (renderer, errorHandler, specs) => { const { signalBus } = renderer; const checkboxInstances = []; @@ -613,7 +1074,7 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy `; container.innerHTML = html; const element = container.querySelector('input[type="checkbox"]'); - const checkboxInstance = { id: `${pluginName$c}-${index2}`, spec, element }; + const checkboxInstance = { id: `${pluginName$b}-${index2}`, spec, element }; checkboxInstances.push(checkboxInstance); } const instances = checkboxInstances.map((checkboxInstance) => { @@ -656,9 +1117,9 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy return instances; } }; - const pluginName$b = "#"; + const pluginName$a = "#"; const commentPlugin = { - name: pluginName$b, + name: pluginName$a, fence: (token) => { const content = token.content.trim(); return sanitizeHtmlComment(content); @@ -865,14 +1326,14 @@ ${reconstitutedRules.join("\n\n")} } return result; } - const pluginName$a = "css"; - const className$9 = pluginClassName(pluginName$a); + const pluginName$9 = "css"; + const className$9 = pluginClassName(pluginName$9); const cssPlugin = { - ...flaggableJsonPlugin(pluginName$a, className$9), + ...flaggableJsonPlugin(pluginName$9, className$9), fence: (token, index2) => { const cssContent = token.content.trim(); const categorizedCss = categorizeCss(cssContent); - return sanitizedHTML("div", { id: `${pluginName$a}-${index2}`, class: className$9 }, JSON.stringify(categorizedCss), true); + return sanitizedHTML("div", { id: `${pluginName$9}-${index2}`, class: className$9 }, JSON.stringify(categorizedCss), true); }, hydrateComponent: async (renderer, errorHandler, specs) => { const cssInstances = []; @@ -894,7 +1355,7 @@ ${reconstitutedRules.join("\n\n")} target.appendChild(styleElement); comments.push(``); cssInstances.push({ - id: `${pluginName$a}-${index2}`, + id: `${pluginName$9}-${index2}`, element: styleElement }); } else { @@ -992,8 +1453,8 @@ ${reconstitutedRules.join("\n\n")} generateRule("hero", "h1"); return cssRules.join("\n\n"); } - const pluginName$9 = "google-fonts"; - const className$8 = pluginClassName(pluginName$9); + const pluginName$8 = "google-fonts"; + const className$8 = pluginClassName(pluginName$8); function inspectGoogleFontsSpec(spec) { var _a, _b; const reasons = []; @@ -1022,7 +1483,7 @@ ${reconstitutedRules.join("\n\n")} }; } const googleFontsPlugin = { - ...flaggableJsonPlugin(pluginName$9, className$8, inspectGoogleFontsSpec), + ...flaggableJsonPlugin(pluginName$8, className$8, inspectGoogleFontsSpec), hydrateComponent: async (renderer, errorHandler, specs) => { const googleFontsInstances = []; let emitted = false; @@ -1093,10 +1554,10 @@ ${reconstitutedRules.join("\n\n")} return instances; } }; - const pluginName$8 = "dropdown"; - const className$7 = pluginClassName(pluginName$8); + const pluginName$7 = "dropdown"; + const className$7 = pluginClassName(pluginName$7); const dropdownPlugin = { - ...flaggableJsonPlugin(pluginName$8, className$7), + ...flaggableJsonPlugin(pluginName$7, className$7), hydrateComponent: async (renderer, errorHandler, specs) => { const { signalBus } = renderer; const dropdownInstances = []; @@ -1119,7 +1580,7 @@ ${reconstitutedRules.join("\n\n")} 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 }; + const dropdownInstance = { id: `${pluginName$7}-${index2}`, spec, element }; dropdownInstances.push(dropdownInstance); } const instances = dropdownInstances.map((dropdownInstance, index2) => { @@ -1220,428 +1681,287 @@ ${reconstitutedRules.join("\n\n")} options = [selected]; } } - } else { - if (selected) { - options = [selected]; - } - } - } - if (!options || options.length === 0) { - return; - } - options.forEach((optionValue) => { - const optionElement = document.createElement("option"); - optionElement.value = optionValue; - optionElement.textContent = optionValue; - let isSelected = false; - if (multiple) { - isSelected = (selected || []).includes(optionValue); - } else { - isSelected = selected === optionValue; - } - optionElement.selected = isSelected; - selectElement.appendChild(optionElement); - }); - } - class DynamicUrl { - constructor(templateUrl, onChange) { - __publicField(this, "signals"); - __publicField(this, "tokens"); - __publicField(this, "lastUrl"); - this.templateUrl = templateUrl; - this.onChange = onChange; - this.signals = {}; - this.tokens = tokenizeTemplate(templateUrl); - const signalNames = this.tokens.filter((token) => token.type === "variable").map((token) => token.name); - if (signalNames.length === 0) { - onChange(templateUrl); - this.lastUrl = templateUrl; - return; - } - signalNames.forEach((signalName) => { - this.signals[signalName] = void 0; - }); - } - makeUrl() { - const signalNames = Object.keys(this.signals); - if (signalNames.length === 0) { - return this.templateUrl; - } - if (this.tokens.length === 1 && this.tokens[0].type === "variable") { - return this.signals[this.tokens[0].name] || ""; - } - const urlParts = []; - this.tokens.forEach((token) => { - if (token.type === "literal") { - urlParts.push(token.value); - } else if (token.type === "variable") { - const signalValue = this.signals[token.name]; - if (signalValue !== void 0) { - urlParts.push(encodeURIComponent(signalValue)); - } - } - }); - return urlParts.join(""); - } - receiveBatch(batch) { - for (const [signalName, batchItem] of Object.entries(batch)) { - if (signalName in this.signals) { - if (batchItem.isData || batchItem.value === void 0) { - continue; - } - this.signals[signalName] = batchItem.value.toString(); - } - } - const newUrl = this.makeUrl(); - if (newUrl !== this.lastUrl) { - this.onChange(newUrl); - this.lastUrl = newUrl; - } - } - } - const ImageOpacity = { - full: "1", - loading: "0.1", - error: "0.5" - }; - const pluginName$7 = "image"; - const className$6 = pluginClassName(pluginName$7); - const imagePlugin = { - ...flaggableJsonPlugin(pluginName$7, className$6), - hydrateComponent: async (renderer, errorHandler, specs) => { - const imageInstances = []; - 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; - container.innerHTML = createImageContainerTemplate("", spec.alt, spec.url, index2, errorHandler); - const { img, spinner, retryBtn, dynamicUrl } = createImageLoadingLogic( - container, - null, - (error) => { - errorHandler(error, pluginName$7, index2, "load", container, img.src); - } - ); - const imageInstance = { - id: `${pluginName$7}-${index2}`, - spec, - img: null, - // Will be set below - spinner: null, - // Will be set below - dynamicUrl - }; - imageInstance.img = img; - imageInstance.spinner = spinner; - if (spec.alt) img.alt = spec.alt; - if (spec.width) img.width = spec.width; - if (spec.height) img.height = spec.height; - imageInstances.push(imageInstance); - } - const instances = imageInstances.map((imageInstance, index2) => { - const { img, spinner, id, dynamicUrl } = imageInstance; - const signalNames = Object.keys((dynamicUrl == null ? void 0 : dynamicUrl.signals) || {}); - return { - id, - initialSignals: Array.from(signalNames).map((name) => ({ - name, - value: null, - priority: -1, - isData: false - })), - destroy: () => { - if (img) { - img.remove(); - } - if (spinner) { - spinner.remove(); - } - }, - receiveBatch: async (batch, from) => { - dynamicUrl == null ? void 0 : dynamicUrl.receiveBatch(batch); - } - }; - }); - return instances; - } - }; - const imgSpinner = ` - - - - - -`; - function createImageContainerTemplate(clasName, alt, src, instanceIndex, errorHandler) { - const tempImg = document.createElement("img"); - if (src.includes("{{")) { - tempImg.setAttribute("src", "data:,"); - tempImg.setAttribute("data-dynamic-url", src); - } else { - if (isSafeImageUrl(src)) { - tempImg.setAttribute("src", src); - } else { - errorHandler(new Error(`Unsafe image URL: ${src}`), pluginName$7, instanceIndex, "load", null, src); - } - } - tempImg.setAttribute("alt", alt); - const imgHtml = tempImg.outerHTML; - return ` - - ${imgHtml} - - `; - } - function createImageLoadingLogic(container, onSuccess, onError) { - container.style.position = "relative"; - const img = container.querySelector("img"); - const spinner = container.querySelector(".image-spinner"); - const retryBtn = container.querySelector(".image-retry"); - const dataDynamicUrl = img.getAttribute("data-dynamic-url"); - img.onload = () => { - spinner.style.display = "none"; - img.style.opacity = ImageOpacity.full; - img.style.display = ""; - retryBtn.style.display = "none"; - img.setAttribute("hasImage", "true"); - }; - img.onerror = () => { - spinner.style.display = "none"; - img.style.opacity = ImageOpacity.error; - img.style.display = "none"; - retryBtn.style.display = ""; - retryBtn.disabled = false; - img.setAttribute("hasImage", "false"); - onError == null ? void 0 : onError(new Error("Image failed to load")); - }; - retryBtn.onclick = () => { - retryBtn.disabled = true; - spinner.style.display = ""; - img.style.opacity = ImageOpacity.loading; - img.style.display = img.getAttribute("hasImage") ? "" : "none"; - const src = img.src; - const onload = img.onload; - const onerror = img.onerror; - img.src = "data:,"; - img.onload = null; - img.onerror = null; - setTimeout(() => { - img.onload = onload; - img.onerror = onerror; - img.src = src; - }, 100); - }; - const result = { img, spinner, retryBtn }; - if (dataDynamicUrl) { - const dynamicUrl = new DynamicUrl(dataDynamicUrl, (src) => { - if (isSafeImageUrl(src)) { - spinner.style.display = ""; - img.src = src; - img.style.opacity = ImageOpacity.loading; - } else { - img.src = ""; - spinner.style.display = "none"; - img.style.opacity = ImageOpacity.full; + } else { + if (selected) { + options = [selected]; } - }); - result.dynamicUrl = dynamicUrl; + } } - return result; + if (!options || options.length === 0) { + return; + } + options.forEach((optionValue) => { + const optionElement = document.createElement("option"); + optionElement.value = optionValue; + optionElement.textContent = optionValue; + let isSelected = false; + if (multiple) { + isSelected = (selected || []).includes(optionValue); + } else { + isSelected = selected === optionValue; + } + optionElement.selected = isSelected; + selectElement.appendChild(optionElement); + }); } - function isSafeImageUrl(url) { - try { - if (url.startsWith("data:image/")) { - const safeMimeTypes = [ - "data:image/png", - "data:image/jpeg", - "data:image/gif", - "data:image/webp", - "data:image/bmp", - "data:image/x-icon" - ]; - for (const mime of safeMimeTypes) { - if (url.startsWith(mime)) { - return true; + const pluginName$6 = "mermaid"; + const className$6 = pluginClassName(pluginName$6); + function inspectMermaidSpec(spec) { + const reasons = []; + let hasFlags = false; + if (spec.diagramText) { + const dangerousPatterns = [ + /javascript:/i, + /