jobmd
v1.0.0
Published
A TypeScript parser for a custom job list markup language
Readme
Job Markdown Parser and Tokenizer
A simple parser for a job markdown language in TypeScript for Node.js 24+.
Installation
npm install jobmdUsage
Basic Usage
import { parse } from './src/parser.ts';
const text = `- MARKETDOPRODUCT-0_1 stream | brand | Project specification | Project initiation #brand #additional_tags @productManager (2026-03-13_20d) 〈mdoValueStream: stream followers: @seniorProjectManager, @streamLead, @sourcing, @projectManager〉
- MARKETDOPRODUCT-0_1_1 stream | brand | Project specification | Presourcing #brand #additional_tags @sourcing (2026-03-13_6d) 〈mdoValueStream: stream followers: @projectManager, @streamLead Parent: MARKETDOPRODUCT-0_1〉`;
const ast = parse(text);
console.log(ast.lines[0].text); // "stream | brand | Project specification | Project initiation"
console.log(ast.lines[0].queue); // "MARKETDOPRODUCT-0_1"
console.log(ast.lines[0].tags); // ["#brand", "#additional_tags"]
console.log(ast.lines[0].user); // "@productManager"
console.log(ast.lines[0].dateRange); // "(2026-03-13_20d)"
console.log(ast.lines[0].children.length); // 1Using the Tokenizer
import { Tokenizer } from './src/tokenizer.ts';
const text = '- MARKETDOPRODUCT-0_1 stream #brand @productManager';
const tokenizer = new Tokenizer(text);
let token = tokenizer.nextToken();
while (token.type !== 'EOF') {
console.log(`${token.type}: ${token.value}`);
console.log(` Position: line ${token.position.line}, column ${token.position.column}`);
console.log(` Bytes: ${token.position.startByte}-${token.position.endByte}`);
token = tokenizer.nextToken();
}Working with AST
import { parse } from './src/parser.ts';
import type { LineNode } from './src/ast.ts';
const ast = parse(text);
// Recursive AST traversal
function traverse(node: LineNode, depth: number = 0) {
const indent = ' '.repeat(depth);
console.log(`${indent}${node.point || ''} ${node.queue || ''} ${node.text}`);
if (node.tags.length > 0) {
console.log(`${indent} Tags: ${node.tags.join(', ')}`);
}
if (node.user) {
console.log(`${indent} User: ${node.user}`);
}
if (node.dateRange) {
console.log(`${indent} Date Range: ${node.dateRange}`);
}
if (node.props.length > 0) {
console.log(`${indent} Props:`);
for (const prop of node.props) {
console.log(`${indent} ${prop.key}: ${prop.values.join(', ')}`);
}
}
if (node.comment) {
console.log(`${indent} Comment: ${node.comment}`);
}
for (const child of node.children) {
traverse(child, depth + 1);
}
}
for (const line of ast.lines) {
traverse(line);
}Language Specification
Tokens
Queue or Ticket:
[A-Z]+-(0|(0_)?[0-9]+)- Examples:
MARKETDOPRODUCT-0_1,FTECH-123,EDUCATION-41472
- Examples:
Tag:
#[a-z]+- Examples:
#brand,#additional_tags
- Examples:
User:
@[a-z][a-z0-9\-]+- Examples:
@productManager,@senior-project-manager
- Examples:
DateRange:
\(<date>_(<date>|\d+[dw])\)- Examples:
(2026-03-13_20d),(2026-03-13_2026-04-02)
- Examples:
Date:
([0-9]{4}-[0-9]{2}-[0-9]{2}|[0-9]{2}.[0-9]{2}.[0-9]{4})- Examples:
2026-03-13,13.03.2026
- Examples:
Props:
〈and〉- key:
[a-zA-Z\-]+ - separator:
: - value:
[^\s]+|"[^"]+"or , or - multiple values separated by
,
- key:
Point:
-or*Indent: Significant, indicates nesting level
Empty Line: Resets nesting level
Comment:
--or//to end of line
Line Structure
(<Point>)? (<Queue>)? <Text> (<Tag> )* (<User>)? (<DateRange>)? (〈(Props)*〉)? (<Comment> <Text>)?Testing
# Run all tests
npm test
# Run tests in watch mode
npm run test:watchBuilding
npm run buildLicense
MIT
