javascript
45 lines · 9 steps
Parsing YAML-style frontmatter in JavaScript
A small parser that pulls a key-value header block off a Markdown document and coerces each value to its natural type.
Explained by
highlit
1const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
2
3function coerce(value) {
4 const trimmed = value.trim();
5 if (trimmed === '') return '';
6 if (trimmed === 'true') return true;
7 if (trimmed === 'false') return false;
8 if (trimmed === 'null' || trimmed === '~') return null;
9
10 if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
11 (trimmed.startsWith("'") && trimmed.endsWith("'"))) {
12 return trimmed.slice(1, -1);
13 }
14
15 if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
16 const inner = trimmed.slice(1, -1).trim();
17 if (inner === '') return [];
18 return inner.split(',').map((item) => coerce(item));
19 }
20
21 if (/^-?\d+$/.test(trimmed)) return parseInt(trimmed, 10);
22 if (/^-?\d*\.\d+$/.test(trimmed)) return parseFloat(trimmed);
23
24 return trimmed;
25}
26
27export function parseFrontmatter(markdown) {
28 const match = markdown.match(FRONTMATTER_RE);
29 if (!match) {
30 return { data: {}, content: markdown };
31 }
32
33 const data = {};
34 for (const line of match[1].split(/\r?\n/)) {
35 if (!line.trim() || line.trimStart().startsWith('#')) continue;
36
37 const sep = line.indexOf(':');
38 if (sep === -1) continue;
39
40 const key = line.slice(0, sep).trim();
41 if (key) data[key] = coerce(line.slice(sep + 1));
42 }
43
44 return { data, content: markdown.slice(match[0].length) };
45}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A single anchored regex can both detect and slice off a structured header in one pass.
- 2Coercion functions turn raw strings into typed values by checking cheap, specific cases first.
- 3Recursing a coercion routine over split parts lets scalar logic handle nested structures like arrays for free.
Related explainers
javascript
const ROLE_PERMISSIONS = { admin: ['users:read', 'users:write', 'billing:read', 'billing:write'], manager: ['users:read', 'billing:read'], member: ['users:read'],
Role-based permissions middleware in Express
authorization
middleware
rbac
Intermediate
9 steps
javascript
function attachThousandSeparators(input, { locale = 'en-US' } = {}) { const formatter = new Intl.NumberFormat(locale); const groupSep = formatter.format(11111).replace(/\d/g, '')[0] || ','; const decimalSep = formatter.format(1.1).replace(/\d/g, '')[0] || '.';
Live thousand separators without losing the caret
dom
intl
caret-preservation
Advanced
8 steps
java
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;
Building a trie for autocomplete in Java
trie
prefix-tree
recursion
Intermediate
8 steps
ruby
class KeyTransformer def self.camelize(data) new.camelize(data) end
Recursively camelizing nested Ruby data
recursion
data-transformation
pattern-matching
Intermediate
7 steps
javascript
import { useReducer, useEffect } from "react"; const initialState = { status: "idle", data: null, error: null };
Building a data-fetching hook in React
custom-hooks
usereducer
data-fetching
Intermediate
9 steps
rust
use std::net::Ipv4Addr; use std::str::FromStr; #[derive(Debug, Clone, Copy)]
Parsing and matching IPv4 CIDR ranges in Rust
bitwise-operations
parsing
error-handling
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/parsing-yaml-style-frontmatter-in-javascript-explained-javascript-749b/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.