markdown-it-flow
A markdown-it plugin that adds MarkdownFlow syntax support to any markdown-it powered application.
Installation
npm install markdown-it markdown-it-flow
# or
yarn add markdown-it markdown-it-flow
# or
pnpm add markdown-it markdown-it-flow
Quick Start
const MarkdownIt = require("markdown-it");
const markdownItFlow = require("markdown-it-flow");
const md = new MarkdownIt();
md.use(markdownItFlow);
const html = md.render(`
# Hello {{user_name}}!
Choose your theme:
?[%{{theme}}Light|Dark|Auto]
`);
Configuration
Plugin Options
const options = {
variableClass: "flow-variable", // CSS class for variables
inputClass: "flow-input", // CSS class for inputs
instructionClass: "flow-instruction", // CSS class for AI instructions
enableVariables: true, // Enable variable parsing
enableInputs: true, // Enable user input parsing
enableInstructions: true, // Enable AI instruction parsing
variableRenderer: null, // Custom variable renderer
inputRenderer: null, // Custom input renderer
instructionRenderer: null, // Custom instruction renderer
};
md.use(markdownItFlow, options);
Usage Examples
Basic HTML Generation
const md = new MarkdownIt().use(markdownItFlow);
const markdown = `
Welcome {{user_name}}!
What's your experience level?
?[%{{level}}Beginner|Intermediate|Advanced]
Generate content appropriate for {{level}} level.
`;
const html = md.render(markdown);
// Output: HTML with proper classes and data attributes
Vue.js Integration
<template>
<div v-html="renderedMarkdown"></div>
</template>
<script>
import MarkdownIt from "markdown-it";
import markdownItFlow from "markdown-it-flow";
export default {
data() {
return {
markdown: "# Hello {{name}}!",
variables: { name: "Vue User" },
};
},
computed: {
renderedMarkdown() {
const md = new MarkdownIt().use(markdownItFlow);
let html = md.render(this.markdown);
// Replace variables
Object.entries(this.variables).forEach(([key, value]) => {
html = html.replace(
new RegExp(
`<span class="flow-variable" data-var="${key}">.*?</span>`,
"g",
),
`<span class="flow-variable">${value}</span>`,
);
});
return html;
},
},
};
</script>
Angular Integration
import { Component } from "@angular/core";
import MarkdownIt from "markdown-it";
import markdownItFlow from "markdown-it-flow";
@Component({
selector: "app-markdown",
template: '<div [innerHTML]="renderedHtml"></div>',
})
export class MarkdownComponent {
private md = new MarkdownIt().use(markdownItFlow);
markdown = "# Welcome {{user}}!";
variables = { user: "Angular Developer" };
get renderedHtml(): string {
let html = this.md.render(this.markdown);
// Process variables
Object.entries(this.variables).forEach(([key, value]) => {
const regex = new RegExp(`{{${key}}}`, "g");
html = html.replace(regex, value as string);
});
return html;
}
}
Vanilla JavaScript
<!doctype html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/markdown-it/dist/markdown-it.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/markdown-it-flow/dist/markdown-it-flow.min.js"></script>
</head>
<body>
<div id="content"></div>
<script>
const md = window.markdownit().use(window.markdownItFlow);
const markdown = `
# Interactive Document
Hello {{user_name}}!
?[%{{choice}}Option A|Option B|Option C]
`;
const html = md.render(markdown);
document.getElementById("content").innerHTML = html;
// Add interactivity
document.querySelectorAll(".flow-input").forEach((input) => {
input.addEventListener("click", (e) => {
if (e.target.tagName === "BUTTON") {
console.log("Selected:", e.target.textContent);
}
});
});
</script>
</body>
</html>
Custom Renderers
Variable Renderer
const options = {
variableRenderer: (tokens, idx) => {
const token = tokens[idx];
const varName = token.content;
return `<input
type="text"
placeholder="${varName}"
data-variable="${varName}"
class="variable-input"
/>`;
},
};
md.use(markdownItFlow, options);
Input Renderer
const options = {
inputRenderer: (tokens, idx) => {
const token = tokens[idx];
const { variable, options } = token.meta;
// Render as radio buttons
let html = `<div class="input-group" data-variable="${variable}">`;
options.forEach((option, i) => {
html += `
<label>
<input type="radio" name="${variable}" value="${option}" />
${option}
</label>
`;
});
html += "</div>";
return html;
},
};
Instruction Renderer
const options = {
instructionRenderer: (tokens, idx) => {
const token = tokens[idx];
const instruction = token.content;
return `
<div class="ai-instruction" data-instruction="${escape(instruction)}">
<i class="ai-icon"></i>
<span>AI will process: ${instruction}</span>
</div>
`;
},
};
Advanced Features
Token Processing
// Access and modify tokens after parsing
const md = new MarkdownIt().use(markdownItFlow);
const tokens = md.parse(markdown);
// Find all variable tokens
const variableTokens = tokens.filter((token) => token.type === "flow_variable");
// Find all input tokens
const inputTokens = tokens.filter((token) => token.type === "flow_input");
// Render with modified tokens
const html = md.renderer.render(tokens, md.options);
State Management
class MarkdownFlowDocument {
constructor() {
this.md = new MarkdownIt().use(markdownItFlow);
this.variables = {};
this.template = "";
}
setTemplate(markdown) {
this.template = markdown;
this.extractVariables();
}
extractVariables() {
const tokens = this.md.parse(this.template);
tokens.forEach((token) => {
if (token.type === "flow_variable") {
this.variables[token.content] = null;
}
});
}
setVariable(name, value) {
this.variables[name] = value;
}
render() {
let html = this.md.render(this.template);
// Replace variables with values
Object.entries(this.variables).forEach(([key, value]) => {
if (value !== null) {
const pattern = new RegExp(
`<span class="flow-variable" data-var="${key}">.*?</span>`,
"g",
);
html = html.replace(
pattern,
`<span class="flow-variable">${value}</span>`,
);
}
});
return html;
}
}
Event Handling
function attachFlowEventHandlers(container) {
// Handle variable inputs
container.querySelectorAll("[data-variable]").forEach((element) => {
element.addEventListener("change", (e) => {
const variable = e.target.dataset.variable;
const value = e.target.value;
console.log(`Variable ${variable} = ${value}`);
// Update your state management here
});
});
// Handle user selections
container.querySelectorAll(".flow-input button").forEach((button) => {
button.addEventListener("click", (e) => {
const variable = e.target.parentElement.dataset.variable;
const value = e.target.textContent;
console.log(`Selected ${variable} = ${value}`);
// Process selection
});
});
}
// Usage
const html = md.render(markdown);
document.getElementById("content").innerHTML = html;
attachFlowEventHandlers(document.getElementById("content"));
Styling
Default CSS
/* Variables */
.flow-variable {
color: #0066cc;
font-weight: bold;
padding: 2px 4px;
background: #e8f4f8;
border-radius: 3px;
}
/* User Inputs */
.flow-input {
margin: 1rem 0;
padding: 1rem;
background: #f5f5f5;
border-radius: 4px;
}
.flow-input button {
margin: 0.25rem;
padding: 0.5rem 1rem;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.flow-input button:hover {
background: #f0f0f0;
}
.flow-input button.selected {
background: #0066cc;
color: white;
}
/* AI Instructions */
.flow-instruction {
border-left: 4px solid #4caf50;
padding-left: 1rem;
margin: 1rem 0;
font-style: italic;
color: #666;
}
TypeScript Support
import MarkdownIt from "markdown-it";
import markdownItFlow, { MarkdownItFlowOptions } from "markdown-it-flow";
const options: MarkdownItFlowOptions = {
variableClass: "custom-variable",
enableVariables: true,
variableRenderer: (tokens, idx) => {
return `<span class="var">${tokens[idx].content}</span>`;
},
};
const md: MarkdownIt = new MarkdownIt();
md.use(markdownItFlow, options);
const html: string = md.render(markdown);
API Reference
Plugin Methods
// Main plugin function
md.use(markdownItFlow, options);
// Access plugin state
const flowState = md.flow;
// Get parsed variables
const variables = flowState.getVariables();
// Get parsed inputs
const inputs = flowState.getInputs();
// Get parsed instructions
const instructions = flowState.getInstructions();
Token Types
// Variable token
{
type: 'flow_variable',
tag: 'span',
content: 'variable_name',
meta: {
original: '{{variable_name}}'
}
}
// Input token
{
type: 'flow_input',
tag: 'div',
content: '',
meta: {
variable: 'choice',
options: ['Option 1', 'Option 2', 'Option 3']
}
}
// Instruction token
{
type: 'flow_instruction',
tag: 'div',
content: 'AI instruction text',
meta: {
type: 'generate' // or 'transform', 'conditional'
}
}
Utility Functions
import { utils } from "markdown-it-flow";
// Parse variables from text
const variables = utils.parseVariables(text);
// Parse user inputs from text
const inputs = utils.parseInputs(text);
// Validate MarkdownFlow syntax
const isValid = utils.validateSyntax(text);
// Interpolate variables
const result = utils.interpolate(template, variables);
Testing
import MarkdownIt from "markdown-it";
import markdownItFlow from "markdown-it-flow";
describe("markdown-it-flow", () => {
let md;
beforeEach(() => {
md = new MarkdownIt().use(markdownItFlow);
});
test("parses variables", () => {
const html = md.render("Hello {{name}}!");
expect(html).toContain('class="flow-variable"');
expect(html).toContain('data-var="name"');
});
test("parses user inputs", () => {
const html = md.render("?[%{{choice}}Yes|No]");
expect(html).toContain('class="flow-input"');
expect(html).toContain('data-variable="choice"');
});
test("custom renderer", () => {
md = new MarkdownIt().use(markdownItFlow, {
variableRenderer: () => "<custom-var></custom-var>",
});
const html = md.render("{{test}}");
expect(html).toContain("<custom-var></custom-var>");
});
});
Performance
Caching
class CachedMarkdownRenderer {
constructor() {
this.md = new MarkdownIt().use(markdownItFlow);
this.cache = new Map();
}
render(markdown, variables = {}) {
const cacheKey = `${markdown}:${JSON.stringify(variables)}`;
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
let html = this.md.render(markdown);
// Apply variables
Object.entries(variables).forEach(([key, value]) => {
html = html.replace(new RegExp(`{{${key}}}`, "g"), value);
});
this.cache.set(cacheKey, html);
return html;
}
clearCache() {
this.cache.clear();
}
}
Streaming
// Process large documents in chunks
function* renderInChunks(markdown, chunkSize = 1000) {
const lines = markdown.split("\n");
const md = new MarkdownIt().use(markdownItFlow);
for (let i = 0; i < lines.length; i += chunkSize) {
const chunk = lines.slice(i, i + chunkSize).join("\n");
yield md.render(chunk);
}
}
// Usage
for (const htmlChunk of renderInChunks(largeMarkdown)) {
// Process each chunk
document.getElementById("content").innerHTML += htmlChunk;
}
Migration
From Standard markdown-it
// Before
const md = new MarkdownIt();
const html = md.render(markdown);
// After
const md = new MarkdownIt().use(markdownItFlow);
const html = md.render(markdown);
// Now supports MarkdownFlow syntax
With Other Plugins
import MarkdownIt from "markdown-it";
import markdownItFlow from "markdown-it-flow";
import markdownItEmoji from "markdown-it-emoji";
import markdownItAnchor from "markdown-it-anchor";
const md = new MarkdownIt()
.use(markdownItEmoji)
.use(markdownItAnchor)
.use(markdownItFlow); // Add MarkdownFlow support
// All plugins work together
const html = md.render(markdown);