- Overview - Back to API reference index
- Compiler Reference - Using the .chalk compiler programmatically
- Library Reference - Creating and managing .chalk libraries
- Primitives Reference - Built-in data types and elements in .chalk
- Type Reference - Complete TypeScript type definitions
- Troubleshooting - Common issues and solutions
Custom Parsers
This guide explains how to create custom parsers for elements and control their execution order using parser priority.
Table of Contents
- Overview
- Parser Priority System
- Creating Custom Parsers
- Priority Guidelines
- Built-in Primitive Priorities
- Common Patterns
- Examples
Overview
Custom parsers allow you to define non-standard syntax for elements. When content is encountered that doesn’t match the standard Chalk syntax, the compiler tries each element’s custom parser to see if it can process the content.
Key Concepts:
- Parser Function: A function that takes a string input and returns a Result object
- Parser Priority: A number that controls the order in which parsers are tried
- Result Object: Either
{ ok: true, data: ElementContent }or{ ok: false, error: string }
Parser Priority System
When multiple parsers could potentially match the same input, parser priority determines which parser is tried first.
How Priority Works
- Parsers are tried in descending priority order (highest priority first)
- Default priority is 5 (if not specified)
- When priorities are equal, parsers are tried in alphabetical order by element name
- First successful match wins - once a parser successfully matches, no other parsers are tried
Why Priority Matters
Without priority control, a generic paragraph parser might match heading content before the heading parser gets a chance to run. Priority ensures more specific parsers are tried before generic catch-all parsers.
Example Problem:
// Without priority, both could match "Hello world"
paragraphParser: matches any text → priority 0
headingParser: matches "# Hello world" → priority 20
// With priority: headingParser tries first, only falls back to paragraph if no match
Creating Custom Parsers
Basic Parser Structure
lib.element({
id: 'myElement',
body: 'literal',
parser: (input) => {
// Try to match your custom syntax
const match = input.match(/your-regex-pattern/);
if (!match) {
// Parsing failed - this parser can't handle this input
return { ok: false, error: 'Expected custom syntax' };
}
// Parsing succeeded - return element data
return {
ok: true,
data: {
instance_id: generateElementId(),
identifier: 'myElement',
body: match[1], // extracted content
detail: null,
attributes: []
}
};
},
parserPriority: 10 // Medium priority
});
Parser Function Signature
parser: (input: string, definition: ElementDefinition) => Result<ElementContent>
Parameters:
input(string): The raw text content to parsedefinition(ElementDefinition): The element definition (for accessing attributes, etc.)
Returns:
- Success:
{ ok: true, data: ChalkElement | ChalkElement[] } - Failure:
{ ok: false, error: string }
Priority Guidelines
Recommended Priority Ranges
| Priority Range | Use Case | Examples |
|---|---|---|
| 20-30 | Very specific, unambiguous syntax | Headings (h1-h6), code fences |
| 10-19 | Moderately specific patterns | Links, lists, blockquotes |
| 5-9 | Generic patterns with some specificity | Bold, italic, inline code |
| 5 (default) | Standard elements | Most custom elements |
| 0-4 | Fallback/catch-all parsers | Paragraphs, plain text |
Choosing the Right Priority
High Priority (20-30):
- Use for syntax that is very unlikely to be mistaken for something else
- Examples:
# Heading,```code```,---horizontal rule
Medium Priority (10-19):
- Use for syntax that has some specificity but could overlap with other patterns
- Examples:
[link](url),- list item,> quote
Default Priority (5):
- Use for most custom elements that don’t conflict with others
- Let the system handle ordering
Low Priority (0-4):
- Use only for catch-all parsers that should run as a last resort
- Examples: paragraph parser that matches any text
Built-in Primitive Priorities
The primitive elements included with every library have the following priorities:
// Headings (h1-h6)
parserPriority: 20
// Paragraphs
parserPriority: 0 // Last resort catch-all
When creating custom elements, consider where they fit in relation to these primitives.
Common Patterns
Pattern 1: Specific Syntax (High Priority)
Use for well-defined, unambiguous syntax:
lib.element({
id: 'callout',
body: 'literal',
attributes: [
{ id: 'type', type: 'string', default: 'info' }
],
parserPriority: 20, // High priority - specific syntax
parser: (input) => {
// Matches: !!! Note: Important message
const match = input.match(/^!!! (.+?):\s*(.+)$/);
if (!match) return { ok: false, error: 'Expected callout syntax' };
return {
ok: true,
data: {
instance_id: generateElementId(),
identifier: 'callout',
body: match[2],
detail: null,
attributes: [
{ instance_id: generateAttributeId(), identifier: 'type', value: match[1].toLowerCase() }
]
}
};
}
});
Pattern 2: Markdown-Style Syntax (Medium Priority)
Use for patterns similar to markdown:
lib.element({
id: 'link',
body: 'literal',
attributes: [
{ id: 'url', type: 'string', required: true },
{ id: 'target', type: 'string' }
],
parserPriority: 15, // Medium priority
parser: (input) => {
// Matches: [text](url)
const match = input.match(/^\[(.+?)\]\((.+?)\)$/);
if (!match) return { ok: false, error: 'Expected [text](url)' };
return {
ok: true,
data: {
instance_id: generateElementId(),
identifier: 'link',
body: match[1], // Link text
detail: null,
attributes: [
{ instance_id: generateAttributeId(), identifier: 'url', value: match[2] }
]
}
};
}
});
Pattern 3: Multiple Matches (Default Priority)
When your parser can match multiple instances:
lib.element({
id: 'item',
body: 'literal',
parserPriority: 5, // Default priority
parser: (input) => {
// Matches multiple items, one per line
const matches = input.match(/^- (.+)$/gm);
if (!matches) return { ok: true, data: [] }; // No matches is ok
return {
ok: true,
data: matches.map(match => ({
instance_id: generateElementId(),
identifier: 'item',
body: match.replace(/^- /, ''),
detail: null,
attributes: []
}))
};
}
});
Pattern 4: Catch-All Parser (Low Priority)
Use only for last-resort parsers:
lib.element({
id: 'text',
body: 'literal',
parserPriority: 0, // Lowest priority - last resort
parser: (input) => {
// Matches any non-empty text
const lines = input.split('\n').filter(line => line.trim());
if (lines.length === 0) return { ok: true, data: [] };
return {
ok: true,
data: lines.map(line => ({
instance_id: generateElementId(),
identifier: 'text',
body: line,
detail: null,
attributes: []
}))
};
}
});
Examples
Example 1: Custom Heading Syntax
lib.element({
id: 'heading',
body: 'literal',
attributes: [
{ id: 'level', type: 'number', default: 1 }
],
parserPriority: 20, // High priority
parser: (input) => {
// Matches: === Heading ===
const match = input.match(/^(=+)\s+(.+?)\s+\1$/);
if (!match) return { ok: false, error: 'Expected === heading ===' };
const level = Math.min(match[1].length, 6);
return {
ok: true,
data: {
instance_id: generateElementId(),
identifier: 'heading',
body: match[2],
detail: null,
attributes: [
{ instance_id: generateAttributeId(), identifier: 'level', value: level }
]
}
};
}
});
Example 2: Admonition Blocks
lib.element({
id: 'admonition',
body: 'literal',
attributes: [
{ id: 'type', type: 'string', default: 'note' },
{ id: 'title', type: 'string' }
],
parserPriority: 18,
parser: (input) => {
// Matches:
// ::: warning Important
// Content here
// :::
const match = input.match(/^:::[ ]*(\w+)(?:[ ]+(.+?))?\n([\s\S]+?)\n:::$/);
if (!match) return { ok: false, error: 'Expected ::: type title\\ncontent\\n:::' };
const attributes = [
{ instance_id: generateAttributeId(), identifier: 'type', value: match[1] }
];
if (match[2]) {
attributes.push(
{ instance_id: generateAttributeId(), identifier: 'title', value: match[2] }
);
}
return {
ok: true,
data: {
instance_id: generateElementId(),
identifier: 'admonition',
body: match[3].trim(),
detail: null,
attributes
}
};
}
});
Example 3: Inline Elements
lib.element({
id: 'emoji',
body: 'literal',
attributes: [
{ id: 'name', type: 'string', required: true }
],
parserPriority: 12,
parser: (input) => {
// Matches: :smile: :heart: :rocket:
const matches = input.match(/:(\w+):/g);
if (!matches) return { ok: true, data: [] };
return {
ok: true,
data: matches.map(match => {
const name = match.slice(1, -1);
return {
instance_id: generateElementId(),
identifier: 'emoji',
body: match,
detail: null,
attributes: [
{ instance_id: generateAttributeId(), identifier: 'name', value: name }
]
};
})
};
}
});
Example 4: Priority Ordering in Practice
import chalk from '@jackhgns/dotchalk';
const lib = chalk.library()
.document({ name: 'doc', body: 'all' })
// Priority 20: Try first - very specific
.element({
id: 'h1',
body: 'literal',
parserPriority: 20,
parser: (input) => {
const match = input.match(/^# (.+)$/gm);
// ... heading logic
}
})
// Priority 15: Try second - moderately specific
.element({
id: 'link',
body: 'literal',
parserPriority: 15,
parser: (input) => {
const match = input.match(/^\[(.+?)\]\((.+?)\)$/);
// ... link logic
}
})
// Priority 5 (default): Try third - standard element
.element({
id: 'custom',
body: 'literal',
// parserPriority defaults to 5
parser: (input) => {
// ... custom logic
}
})
// Priority 0: Try last - catch-all
.element({
id: 'paragraph',
body: 'literal',
parserPriority: 0,
parser: (input) => {
const matches = input.match(/^(?!# ).+$/gm);
// ... paragraph logic (matches anything not a heading)
}
});
// When parsing "# Hello", parsers are tried in order:
// 1. h1 (priority 20) → matches! ✓
// 2. link, custom, paragraph not tried
// When parsing "Regular text", parsers are tried in order:
// 1. h1 (priority 20) → no match
// 2. link (priority 15) → no match
// 3. custom (priority 5) → no match
// 4. paragraph (priority 0) → matches! ✓
Best Practices
- Be Specific: Use high priority only for truly unambiguous syntax
- Test Conflicts: If two parsers could match the same input, the higher priority wins
- Start with Default: Use priority 5 (default) unless you have a specific reason to change it
- Document Your Choices: Comment why you chose a particular priority
- Return Empty Arrays: For multi-match parsers, return
{ ok: true, data: [] }if no matches found - Fail Fast: Return
{ ok: false, error: '...' }if input definitely doesn’t match your syntax