remark-flow
A remark plugin that adds MarkdownFlow syntax support to react-markdown.
Installation
npm install remark-flow react-markdown
# or
yarn add remark-flow react-markdown
# or
pnpm add remark-flow react-markdown
Quick Start
import ReactMarkdown from "react-markdown";
import remarkFlow from "remark-flow";
function App() {
const markdown = `
# Hello {{user_name}}!
Choose your preference:
?[%{{theme}}Light|Dark|Auto]
`;
return (
<ReactMarkdown
remarkPlugins={[remarkFlow]}
components={{
variable: ({ name, value }) => <span>{value || `{{${name}}}`}</span>,
userInput: ({ variable, options, onSelect }) => (
<select onChange={(e) => onSelect(e.target.value)}>
{options.map((opt) => (
<option key={opt}>{opt}</option>
))}
</select>
),
}}
>
{markdown}
</ReactMarkdown>
);
}
Configuration
Plugin Options
import remarkFlow from "remark-flow";
const options = {
variablePrefix: "{{", // Variable start delimiter
variableSuffix: "}}", // Variable end delimiter
inputPrefix: "?[%{{", // User input start
inputSuffix: "}}", // User input end
enableAIInstructions: true, // Parse AI instruction blocks
strict: false, // Strict parsing mode
};
<ReactMarkdown remarkPlugins={[[remarkFlow, options]]}>
{markdown}
</ReactMarkdown>;
Component Customization
Variable Component
const components = {
variable: ({ name, value, context }) => {
// name: variable name (e.g., "user_name")
// value: current value if available
// context: additional context data
return (
<span className="variable" title={name}>
{value || <em>{name}</em>}
</span>
);
},
};
User Input Component
const components = {
userInput: ({ variable, options, onSelect, selected }) => {
return (
<div className="user-input">
{options.map((option) => (
<button
key={option}
onClick={() => onSelect(option)}
className={selected === option ? "selected" : ""}
>
{option}
</button>
))}
</div>
);
},
};
AI Instruction Component
const components = {
aiInstruction: ({ content, variables, type }) => {
// content: The instruction text
// variables: Variables referenced in the instruction
// type: Instruction type (generate, transform, etc.)
return (
<div className="ai-instruction">
<strong>AI:</strong> {content}
</div>
);
},
};
State Management
With React State
function InteractiveDoc() {
const [variables, setVariables] = useState({
user_name: "Guest",
theme: "light",
});
const handleVariableChange = (name, value) => {
setVariables((prev) => ({ ...prev, [name]: value }));
};
return (
<ReactMarkdown
remarkPlugins={[remarkFlow]}
components={{
variable: ({ name }) => <span>{variables[name] || `{{${name}}}`}</span>,
userInput: ({ variable, options }) => (
<select
onChange={(e) => handleVariableChange(variable, e.target.value)}
>
{options.map((opt) => (
<option key={opt}>{opt}</option>
))}
</select>
),
}}
>
{markdown}
</ReactMarkdown>
);
}
With Context API
const MarkdownContext = createContext();
function MarkdownProvider({ children }) {
const [variables, setVariables] = useState({});
return (
<MarkdownContext.Provider value={{ variables, setVariables }}>
{children}
</MarkdownContext.Provider>
);
}
function VariableRenderer({ name }) {
const { variables } = useContext(MarkdownContext);
return <span>{variables[name] || `{{${name}}}`}</span>;
}
With Redux
import { useSelector, useDispatch } from "react-redux";
function MarkdownWithRedux({ content }) {
const variables = useSelector((state) => state.markdown.variables);
const dispatch = useDispatch();
return (
<ReactMarkdown
remarkPlugins={[remarkFlow]}
components={{
variable: ({ name }) => <span>{variables[name]}</span>,
userInput: ({ variable, options }) => (
<select
onChange={(e) =>
dispatch({
type: "SET_VARIABLE",
name: variable,
value: e.target.value,
})
}
>
{options.map((opt) => (
<option key={opt}>{opt}</option>
))}
</select>
),
}}
>
{content}
</ReactMarkdown>
);
}
Advanced Features
Custom Node Types
import remarkFlow from "remark-flow";
const customPlugin = [
remarkFlow,
{
customNodes: {
timer: {
pattern: /!T\[([^\]]+)\]/,
component: "timer",
},
progress: {
pattern: /%P\[([0-9]+)\]/,
component: "progress",
},
},
},
];
const components = {
timer: ({ duration }) => <Timer seconds={duration} />,
progress: ({ value }) => <ProgressBar percent={value} />,
};
Conditional Rendering
function ConditionalContent({ markdown }) {
const [variables, setVariables] = useState({});
const processConditionals = (content) => {
// Process if statements in markdown
return content.replace(
/If {{(\w+)}} is "([^"]+)":\s*([^]*?)(?=If |$)/g,
(match, varName, value, body) => {
if (variables[varName] === value) {
return body;
}
return "";
},
);
};
return (
<ReactMarkdown remarkPlugins={[remarkFlow]} components={components}>
{processConditionals(markdown)}
</ReactMarkdown>
);
}
AI Processing Integration
function AIProcessedMarkdown({ template }) {
const [content, setContent] = useState(template);
const [variables, setVariables] = useState({});
const [processing, setProcessing] = useState(false);
const processWithAI = async () => {
setProcessing(true);
const response = await fetch("/api/process", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ template, variables }),
});
const result = await response.json();
setContent(result.processedContent);
setProcessing(false);
};
useEffect(() => {
if (Object.keys(variables).length > 0) {
processWithAI();
}
}, [variables]);
return (
<div>
{processing && <div>Processing with AI...</div>}
<ReactMarkdown
remarkPlugins={[remarkFlow]}
components={{
userInput: ({ variable, options }) => (
<select
onChange={(e) =>
setVariables((prev) => ({
...prev,
[variable]: e.target.value,
}))
}
>
<option>Select...</option>
{options.map((opt) => (
<option key={opt}>{opt}</option>
))}
</select>
),
}}
>
{content}
</ReactMarkdown>
</div>
);
}
Styling
CSS Classes
The plugin adds semantic classes to elements:
/* Variables */
.markdown-flow-variable {
color: #0066cc;
font-weight: bold;
}
/* User inputs */
.markdown-flow-input {
margin: 1rem 0;
padding: 1rem;
background: #f5f5f5;
border-radius: 4px;
}
/* AI instructions */
.markdown-flow-instruction {
border-left: 4px solid #4caf50;
padding-left: 1rem;
margin: 1rem 0;
}
Styled Components
import styled from "styled-components";
const StyledVariable = styled.span`
color: ${(props) => (props.hasValue ? "#333" : "#999")};
background: ${(props) => (props.hasValue ? "#e8f4f8" : "#f5f5f5")};
padding: 2px 6px;
border-radius: 3px;
`;
const components = {
variable: ({ name, value }) => (
<StyledVariable hasValue={!!value}>{value || name}</StyledVariable>
),
};
TypeScript Support
import ReactMarkdown from 'react-markdown';
import remarkFlow, { RemarkFlowOptions, FlowComponents } from 'remark-flow';
const options: RemarkFlowOptions = {
variablePrefix: '{{',
variableSuffix: '}}',
strict: true
};
const components: FlowComponents = {
variable: ({ name, value }: { name: string; value?: string }) => (
<span>{value || name}</span>
),
userInput: ({ variable, options, onSelect }) => (
<select onChange={(e) => onSelect(e.target.value)}>
{options.map(opt => <option key={opt}>{opt}</option>)}
</select>
)
};
function App() {
return (
<ReactMarkdown
remarkPlugins={[[remarkFlow, options]]}
components={components}
>
{markdown}
</ReactMarkdown>
);
}
Testing
import { render, screen } from "@testing-library/react";
import ReactMarkdown from "react-markdown";
import remarkFlow from "remark-flow";
describe("MarkdownFlow Rendering", () => {
test("renders variables", () => {
const markdown = "Hello {{name}}!";
render(
<ReactMarkdown
remarkPlugins={[remarkFlow]}
components={{
variable: ({ name }) => (
<span data-testid={`var-${name}`}>{name}</span>
),
}}
>
{markdown}
</ReactMarkdown>,
);
expect(screen.getByTestId("var-name")).toBeInTheDocument();
});
test("renders user inputs", () => {
const markdown = "?[%{{choice}}Yes|No]";
render(
<ReactMarkdown
remarkPlugins={[remarkFlow]}
components={{
userInput: ({ options }) => (
<div data-testid="input">
{options.map((opt) => (
<button key={opt}>{opt}</button>
))}
</div>
),
}}
>
{markdown}
</ReactMarkdown>,
);
expect(screen.getByText("Yes")).toBeInTheDocument();
expect(screen.getByText("No")).toBeInTheDocument();
});
});
Performance
Memoization
import { memo, useMemo } from "react";
const MemoizedMarkdown = memo(({ content, variables }) => {
const processedContent = useMemo(() => {
// Process content with variables
return interpolateVariables(content, variables);
}, [content, variables]);
return (
<ReactMarkdown remarkPlugins={[remarkFlow]}>
{processedContent}
</ReactMarkdown>
);
});
Lazy Loading
import { lazy, Suspense } from "react";
const LazyMarkdown = lazy(() =>
import("react-markdown").then((module) => ({
default: module.default,
})),
);
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyMarkdown remarkPlugins={[remarkFlow]}>{content}</LazyMarkdown>
</Suspense>
);
}
Migration Guide
From Standard Markdown
// Before
<ReactMarkdown>{markdown}</ReactMarkdown>;
// After
import remarkFlow from "remark-flow";
<ReactMarkdown remarkPlugins={[remarkFlow]} components={flowComponents}>
{markdown}
</ReactMarkdown>;
From Other Plugins
// With multiple plugins
import remarkGfm from "remark-gfm";
import remarkFlow from "remark-flow";
<ReactMarkdown
remarkPlugins={[
remarkGfm,
remarkFlow,
// Other plugins...
]}
>
{markdown}
</ReactMarkdown>;
API Reference
Plugin Exports
import remarkFlow, {
parseVariables,
parseUserInputs,
createFlowProcessor,
defaultOptions,
} from "remark-flow";
// Parse variables from markdown
const vars = parseVariables(markdown);
// Parse user inputs
const inputs = parseUserInputs(markdown);
// Create custom processor
const processor = createFlowProcessor(options);
Utilities
import { utils } from "remark-flow";
// Interpolate variables
const result = utils.interpolate(template, variables);
// Validate syntax
const isValid = utils.validate(markdown);
// Extract metadata
const meta = utils.extractMetadata(markdown);