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,
+ /