import { marked } from 'marked'; import { MarkdownOptions } from './render'; export interface DescriptionListToken extends marked.Tokens.Generic { type: 'description_list'; items: (DescriptionTermToken | DescriptionDetailToken)[]; } export type DescriptionElemToken = DescriptionTermToken | DescriptionDetailToken; export interface DescriptionTermToken extends marked.Tokens.Generic { type: 'description_term'; text: string; } export interface DescriptionDetailToken extends marked.Tokens.Generic { type: 'description_detail'; text: string; } export function description_list_ext(renderer: marked.Renderer, opts: MarkdownOptions) : marked.TokenizerExtension & marked.RendererExtension { return { name: 'description_list', level: 'block', start: (src) => src.match(/^: /)?.index, tokenizer(src, tokens) { const start = src.match(/^: /)?.index; const lines = src.slice(start).split(/\n/g); const token: DescriptionListToken = { type: 'description_list', raw: '', items: [ ] }; let current: DescriptionElemToken; const render_current = () => { if (current) { this.lexer.blockTokens(current.text, current.tokens); current = null; } }; for (const line of lines) { // Skip empty lines if (! line.trim()) { token.raw += line + '\n'; if (current) { current.raw += line + '\n'; current.text += '\n'; } continue; } // If the line starts immediately with a colon, it is a
if (line.startsWith(': ')) { render_current(); token.raw += line + '\n'; token.items.push( current = { type: 'description_term', raw: line, text: line.slice(2), tokens: [ ], } ); continue; } // If the line starts with a colon after an indent, it is a
if (line.startsWith(' : ')) { render_current(); token.raw += line + '\n'; token.items.push( current = { type: 'description_detail', raw: line, text: line.slice(4), tokens: [ ], } ); continue; } // If the line starts with (at least) two indents, it is a child // of the current element if (line.startsWith(' ')) { token.raw += line + '\n'; current.raw += '\n' + line; current.text += '\n' + line.slice(current.type === 'description_term' ? 2 : 4); continue; } // If the line starts with one indent, it is a child of the current //
(but is not allowed after a
) if (line.startsWith(' ')) { if (current.type !== 'description_term') { render_current(); break; } token.raw += line + '\n'; current.raw += '\n' + line; current.text += '\n' + line.slice(2); continue; } // If the line starts any other way, it is the start of new content // and we are done parsing render_current(); break; } render_current(); if (token.items.length) { return token; } }, renderer(token: DescriptionListToken) { const items = token.items.map((item) => { const tag = item.type === 'description_term' ? 'dt' : 'dd'; return `<${tag}>${this.parser.parse(item.tokens)}`; }); return `
${items.join('')}
`; } }; }