Variable Extractor
Paste any prompt template and extract {{variables}} — see counts, spot duplicates, fill in test values, and browse a template library. 100% client-side, nothing leaves your browser.
Extract Variables
Paste a prompt template below. The extractor automatically finds all {{variables}} and shows stats.
Fill & Live Preview
Edit the template on the left, fill variable values, and see the rendered result on the right. Changes update instantly.
Template
Filled Output
Fill values on the left to see the result here.
Template Library
Browse ready-to-use prompt templates. Click Use Template to load one into the Extract tab, or Load & Fill to open it in the Fill tab.
How the Variable Extractor Works
A step-by-step walkthrough of the variable extraction engine, fill & preview system, and template library architecture.
Core Extraction — Regex & Variable Table
Why learn this
Prompt templates use {{variables}} for dynamic content — roles, tasks, context, audience. Manually tracking variables across large templates is error-prone. A regex-based extractor gives you instant visibility into every variable, its count, and its duplicates, making template debugging and maintenance much faster.
How the regex works
The core pattern /\{\{\s*([\w.-]+)\s*\}\}/g matches double-brace syntax with optional whitespace. The capture group ([\w.-]+) extracts the variable name — allowing word characters, dots, and hyphens. This covers common naming conventions like {{role}}, {{word_count}}, and {{user.email}}.
Key concepts
[...text.matchAll(regex)]— Returns all matches as iterables. Each match includes the full pattern and capture groupsMap(name → {name, count})— Tracks unique variable names and occurrence counts. O(1) lookup for dedup detection- Fill rate calculation —
filled / total * 100. A variable is "filled" whensharedValues.get(name)?.trim().length > 0 - Duplicate detection —
total - uniquegives duplicate count. Highlighted with an amber badge in the table
Alternative approaches
- Custom delimiters — Support
{% var %}(Jinja),$var$(Mustache), or<%= var %>(EJS). Change the regex pattern accordingly - AST-based parsing — For complex templates with conditionals and loops, a full parser is needed. Regex has limits with nested structures
- Server-side extraction — Send templates to a backend for validation. Adds latency and requires API keys
Browser compatibility
matchAll(): Chrome 73+, Firefox 67+, Safari 13+, Edge 79+. IE11 needsexec()loop polyfillMap/Set: Chrome 38+, Firefox 13+, Safari 7.1+, Edge 12+. IE11 not supported- CSS Grid: Chrome 57+, Firefox 52+, Safari 10.1+, Edge 16+
Performance notes
- Regex match runs in O(n). A 2KB template completes in ~0.05ms
- Variable table re-renders via innerHTML in under 0.5ms for 50+ variables
- Zero DOM allocations after initial render
Common pitfalls
- Whitespace in variable names —
{{ my var }}captures " my var " including spaces. Consider trimming after extraction - Nested braces in text — Literal double braces (e.g., JSON inside the prompt) get incorrectly flagged. Use escape sequences or lookbehinds
- Empty variable names —
{{}}matches the pattern but produces an empty name. Skip withif (!name) continue
const VAR_REGEX = /\{\{\s*([\w.-]+)\s*\}\}/g; function extractVariables(text) {
const vars = new Map();
const matches = [...text.matchAll(VAR_REGEX)];
for (const m of matches) {
const name = m[1].trim();
if (!name) continue;
if (!vars.has(name))
vars.set(name, { name, count: 0 });
vars.get(name).count++;
}
return vars;
} function getFillRateVal(vars) {
let filled = 0;
for (const [name] of vars) {
if (sharedValues.get(name)
&& sharedValues.get(name)
.trim().length > 0) filled++;
}
return vars.size === 0
? 0
: Math.round((filled / vars.size) * 100);
} The matchAll() method returns an iterator, not an array. Spreading into [...iterable] is the cleanest way to work with all matches. For large templates (>100KB), use a for...of loop instead to avoid creating an intermediate array.
Fill & Preview — Live Template Rendering
Why learn this
A template is only useful when filled. The Fill & Preview tab shows how a prompt looks with real values before sending it to an LLM. The live preview updates on every keystroke — no save button, no form submission, just instant feedback. This pattern is used by tools like Anthropic Console and OpenAI Playground.
How the fill engine works
The fill function uses String.replace() with the same VAR_REGEX. For each match, it looks up the variable name in the shared values map. If a value exists and is non-empty, it replaces the placeholder. Otherwise, the placeholder is preserved — so you can see which variables still need values.
The cross-tab shared state (sharedValues, a Map) means changing a value in the Extract tab automatically updates the Fill tab and vice versa. This makes the two tabs feel like a single tool rather than two separate modes.
Key concepts
text.replace(regex, (match, name) => values.get(name) || match)— The replacer function returns the stored value if it exists, otherwise preserves the original placeholder- Shared state via
Map— Single source of truth for all variable values across both tabs navigator.clipboard.writeText()— Modern clipboard API with execCommand fallback- Side-by-side layout — CSS Grid with
1fr 1fr. Template on the left, filled output on the right
Alternative approaches
- Debounced sync — Debounce at 150ms instead of updating on every keystroke. Reduces renders for very large templates
- Diff-based rendering — Compute a text diff and update only changed segments instead of re-rendering the entire preview
- In-place editing — Click-to-edit variables directly in the template textarea. More intuitive but harder to implement
Browser compatibility
String.replace()with replacer function: supported in all browsersnavigator.clipboard.writeText(): Chrome 66+, Firefox 63+, Safari 13.1+. Falls back toexecCommand('copy')white-space: pre-wrap: supported in all browsers. Preserves newlines and spaces
Common pitfalls
- Replacing inside code blocks — If the template contains code with double braces (CSS, template literals), the regex incorrectly replaces them. Filter out
```-delimited code blocks before extraction - Partial values — A value of a single space or period counts as "filled". Add
.trim()before the length check - XSS via variable values — Using
textContent(not innerHTML) prevents HTML injection. If switching to formatted output, sanitize inputs
function fillTemplate(text, values) {
return text.replace(VAR_REGEX,
(match, name) => {
const val = values.get(name);
return val && val.trim().length > 0
? val : match;
}
);
} function renderFillTab() {
const text = fillTemplateInput.value;
const vars = extractVariables(text);
const values = new Map(sharedValues);
// Build input rows for each variable
let inputsHtml = '';
for (const [name] of vars) {
const val = values.get(name) || '';
inputsHtml += '<div class="fill-var-row">'
+ '<span>{{' + name + '}}</span>'
+ '<input value="' + val + '" />'
+ '</div>';
}
fillVarInputs.innerHTML = inputsHtml;
// Fill and show output
const filled = fillTemplate(text, values);
filledOutput.textContent = filled;
fillCharCount.textContent
= filled.length + ' chars';
} The new Map(sharedValues) pattern creates a snapshot of shared values at render time. Without this, mutating a value during rendering would cause inconsistent state. Always copy before iterating if you're modifying and rendering in the same function.
Template Library — Presets & Category System
Why learn this
A template library transforms a single-purpose tool into a reusable resource. Instead of typing prompts from scratch, users browse pre-built templates across categories — Writing, Code, Business, Analysis, Creative — and load them with one click. This section is built in under 200 lines of vanilla JS with zero dependencies.
How the library works
The library stores 8 preset templates as a static array of objects. Each template has: id, name, category, description, template, and tips. Categories are extracted dynamically by iterating over the array and collecting unique values into a Set.
The grid view shows all templates matching the active category filter and search query. Clicking a template card opens an expanded view showing the full template body, variable inputs, and usage tips. "Use Template" loads it into the Extract tab; "Load & Fill" loads it into the Fill tab.
Key concepts
- Static data array — 8 templates defined inline. No database, no API, no build step needed
- Category extraction via
Set—[...new Set(templates.map(t => t.category))].sort(). Automatically adapts to new categories - Dual-action cards — "Use Template" (Extract) and "Load & Fill" (Fill). Same template data, different routes
- Expanded view with live variable inputs — Same shared values map, same input binding pattern
Alternative approaches
- External template store — Fetch templates from a JSON file or API. Enables adding templates without rebuilding the page
- User-created templates — Save custom templates to
localStorage. Merge presets and custom templates in the same grid - Template versioning — Store template history with diffs. Overkill for this tool but relevant for team-shared libraries
Browser compatibility
Array.filter()/Array.sort(): supported in all browsers since IE9+Setfor category dedup: Chrome 38+, Firefox 13+, Safari 7.1+, Edge 12+Element.closest(): Chrome 41+, Firefox 35+, Safari 6+, Edge 15+. Used for card click delegation
Common pitfalls
- Template ID collisions — Each template needs a unique
id. Use a naming convention likecategory-nameto ensure uniqueness - Variable count mismatch — The grid shows counts derived from raw template text. If extraction logic changes, grid counts might be stale
- Search reset on category change — The search input keeps its value when changing category but the grid re-renders. Clear search on category change for a cleaner UX
const TEMPLATES = [
{
id: 'blog-post',
name: 'Blog Post Writer',
category: 'Writing',
description: 'Generate a
blog post with topic...',
template: `You are a {{role}}
writing about {{topic}}...`,
tips: 'Try tone="conversational"'
},
// ... 7 more templates
]; function renderLibGrid() {
const q = libQuery.toLowerCase().trim();
const filtered = TEMPLATES.filter(t => {
const catMatch = libCategory === 'All'
|| t.category === libCategory;
const searchMatch = !q
|| t.name.toLowerCase().includes(q)
|| t.description.includes(q);
return catMatch && searchMatch;
});
libGrid.innerHTML = filtered.map(t => `
<div class="lib-card">
<h4>${t.name}</h4>
...
</div>
`).join('');
} The category filter buttons use a render-and-rebind pattern: every time the active category changes, all filter buttons are recreated from scratch. This is simpler than tracking active state with class toggles and avoids stale event listeners. For 3-5 categories, re-rendering is faster than diffing.
Lessons Learned — Build Process
Design insights and development notes from building the Variable Extractor.
The variable extraction regex /\{\{\s*([\w.-]+)\s*\}\}/g handles 95% of real-world prompt templates. Keep the capture group narrow ([\w.-]+) and the outer pattern flexible (\s*). This catches {{ role }} and {{user.email}} without false positives on surrounding text.
The sharedValues map is the single source of truth for all variable values across all three tabs. When a user fills a variable in the Extract tab, it is immediately available in the Fill tab. This cross-tab state sharing is implemented with zero framework overhead — just a Map and re-render calls.
The fill engine preserves {{unset_variable}} markers when a value has not been provided. This is critical: the user can see exactly which variables still need values. An alternative implementation that removes unfilled placeholders would make it harder to spot gaps.
The 8 preset templates transform the tool from a debugging utility into a prompt engineering resource. Users can browse, compare, and learn from pre-built templates. The "Use Template" / "Load & Fill" dual actions make it zero-friction to start working with any template.
Each tab is independently useful but together they form a complete prompt engineering workflow: extract variables, fill values, preview output, and browse templates. The shared state makes the tabs feel like one cohesive tool rather than three separate features.