UNPKG

hackmd-to-html-cli

Version:

A node.js CLI tool for converting HackMD markdown to HTML.

202 lines (189 loc) 9.1 kB
import MarkdownIt, { StateCore, Token } from 'markdown-it' import { MyToken } from './token' export enum BlockquoteTokenProperty { name, time, color, text, blockquoteXStart, blockquoteXEnd } export class BlockquoteToken { public property: BlockquoteTokenProperty public value: string constructor(property: BlockquoteTokenProperty, value: string) { this.property = property this.value = value } } export function parseBlockquoteParams(src: string): BlockquoteToken[] { // [name=ChengHan Wu] [time=Sun, Jun 28, 2015 10:00 PM] [color=red] const pattern = /\[(.*?)=(.*?)\]/g const matching = [...src.matchAll(pattern)] if (matching.length === 0) return [] const tokens: BlockquoteToken[] = [] let j = 0; let blockquoteStart = false; // decide whether close for (let i = 0; i < matching.length; i++) { const property: string = matching[i]![1]?.trim() ?? '' const value: string = matching[i]![2] ?? '' const textLen: number = matching[i]![0].length ?? 0 const pos: number = matching[i]?.index ?? 0 if (pos > j) { if (blockquoteStart) { tokens.push(new BlockquoteToken(BlockquoteTokenProperty.blockquoteXEnd, '')) blockquoteStart = false } // normal text tokens.push(new BlockquoteToken(BlockquoteTokenProperty.text, src.substring(j, pos))) } switch (property) { case 'name': if (tokens.length == 0 || tokens[tokens.length - 1]?.property === BlockquoteTokenProperty.text) { if (blockquoteStart) { tokens.push(new BlockquoteToken(BlockquoteTokenProperty.blockquoteXEnd, '')) } tokens.push(new BlockquoteToken(BlockquoteTokenProperty.blockquoteXStart, '')) blockquoteStart = true } tokens.push(new BlockquoteToken(BlockquoteTokenProperty.name, value.trimStart())) break case 'time': if (tokens.length == 0 || tokens[tokens.length - 1]?.property === BlockquoteTokenProperty.text) { if (blockquoteStart) { tokens.push(new BlockquoteToken(BlockquoteTokenProperty.blockquoteXEnd, '')) } tokens.push(new BlockquoteToken(BlockquoteTokenProperty.blockquoteXStart, '')) blockquoteStart = true } tokens.push(new BlockquoteToken(BlockquoteTokenProperty.time, value.trimStart())) break case 'color': tokens.push(new BlockquoteToken(BlockquoteTokenProperty.color, value.trimStart())) break default: tokens.push(new BlockquoteToken(BlockquoteTokenProperty.text, value)) break } j = pos + textLen + 1 } if (blockquoteStart) { tokens.push(new BlockquoteToken(BlockquoteTokenProperty.blockquoteXEnd, '')) blockquoteStart = false } if (src.length != j) { tokens.push(new BlockquoteToken(BlockquoteTokenProperty.text, src.substring(j, src.length))) } return tokens } export function MarkdownItBlockquoteX(md: MarkdownIt) { function blockquoteX(state: StateCore): void { const blockTokens = state.tokens let detect = false let blockquoteOpenAt = 0 for (let j = 0; j < blockTokens.length; j++) { if (blockTokens[j]!.type === 'blockquote_open') { blockquoteOpenAt = j detect = true } if (blockTokens[j]!.type === 'blockquote_close') { detect = false } if (!detect) { continue } if (blockTokens[j]!.type === 'inline') { for (let n = 0; n < blockTokens[j]!.children!.length; n++) { // find text token if (blockTokens[j]!.children![n]!.type !== "text") continue; // avoid matching same children[n] let nextN = n // try to parse const m = parseBlockquoteParams(blockTokens[j]!.children![n]!.content) if (m.length === 0) continue // render const newTokens: Token[] = [] // when the content is // [invalid] [name] [time] // we want to render // // <span>[invalid]<span> // <span class="blockquoteX"> // <span class="material-symbols-outlined material-symbols-outlined-fill"> // name // </span> // <span class="material-symbols-outlined"> // time // </span> // </span> // we only add the class `blockquoteX` before the `name` or `time` // In hackMD, it only supports the format [name=blablabla] or [name= blablabla]. // However, in hmd2html, we use looser constraints, meaning it supports spaces between words. // e.g. [ name = blablabla ] let token: Token // parse name, time, color let color = ''; for (let s = 0; s < m.length; s++) { const property: BlockquoteTokenProperty | undefined = m[s]?.property const value: string = m[s]?.value ?? '' switch (property) { case BlockquoteTokenProperty.blockquoteXStart: token = new MyToken('blockquoteX_open', 'span', 1) token.attrs = [['class', 'blockquoteX']] newTokens.push(token) break case BlockquoteTokenProperty.blockquoteXEnd: token = new MyToken('blockquoteX_close', 'span', -1) newTokens.push(token) break case BlockquoteTokenProperty.name: token = new MyToken('blockquoteX_name_open', 'span', 1) token.attrs = [['class', 'material-symbols-outlined material-symbols-outlined-fill']] newTokens.push(token) token = new MyToken('text', '', 0) nextN++ token.content = 'person' newTokens.push(token) token = new MyToken('blockquoteX_name_close', 'span', -1) newTokens.push(token) token = new MyToken('text', '', 0) nextN++ token.content = value.trim() newTokens.push(token) break case BlockquoteTokenProperty.time: token = new MyToken('blockquoteX_date_open', 'span', 1) token.attrs = [['class', 'material-symbols-outlined']] newTokens.push(token) token = new MyToken('text', '', 0) nextN++ token.content = 'schedule' newTokens.push(token) token = new MyToken('blockquoteX_date_close', 'span', -1) newTokens.push(token) token = new MyToken('text', '', 0) nextN++ token.content = value.trim() newTokens.push(token) break case BlockquoteTokenProperty.text: token = new MyToken('text', '', 0) nextN++ token.content = value newTokens.push(token) break case BlockquoteTokenProperty.color: color = value ?? '' } } blockTokens[j]!.children = md.utils.arrayReplaceAt(blockTokens[j]!.children!, n, newTokens) if (color != '') { blockTokens[blockquoteOpenAt]!.attrs = [['style', 'border-color:' + color + ';']] } n = nextN } } } } md.core.ruler.push('blockquoteX', blockquoteX) }