UNPKG

@tdb/web

Version:

Common condiguration for serving a web-site and testing web-based UI components.

156 lines (135 loc) 4.12 kB
import { fs, value as valueUtil } from '../common'; import { IMarkdown, IMarkdownSection, IParseResponse, IManifestMarkdown, } from './types'; import { parseYaml } from './util.parse'; export type IMarkdownResponse = IParseResponse<IMarkdown>; /** * See: * https://unified.js.org/ * https://remark.js.org/ */ const unified = require('unified'); const parse = require('remark-parse'); const html = require('remark-html'); const processor = unified() .use(parse, { commonmark: true }) .use(html); /** * Parses a markdown body and packages it as a manifest return type. */ export async function toManifestMarkdown( path: string, body?: string, ): Promise<IManifestMarkdown> { const res = body ? await parseMarkdown(body) : undefined; const markdown = res ? res.data : undefined; const error = res ? res.error : undefined; const result: IManifestMarkdown = { type: 'FILE/markdown', path, markdown, error, }; return valueUtil.deleteUndefined(result); } /** * Loads a markdown file and parses it. */ export async function parseMarkdownFile( path: string, ): Promise<IMarkdownResponse> { try { const text = (await fs.readFile(path)).toString(); return parseMarkdown(text); } catch (err) { let message = `Failed to parse markdown while loading file.`; message = `${message} ${err.message}`; return { data: undefined, error: { message } }; } } /** * Parses markdown into structure, converting YAML blocks * into usable data. */ export async function parseMarkdown(text: string): Promise<IMarkdownResponse> { const result: IMarkdownResponse = {}; type IMdAst = { type: string; value: string; lang?: string; meta?: string; depth?: number; }; try { const ast = (await processor.parse(text)) as { children: IMdAst[] }; // Convert to raw AST parts. const parts = ast.children.map((child, index) => { const { type, lang, value, depth } = child; const meta = child.meta ? child.meta : undefined; const html = processor.stringify(child); let data: object | undefined; let error: IMarkdownResponse['error'] | undefined; if (type === 'code' && lang === 'yaml' && value) { const res = parseYaml(value); data = res.data; error = res.error; } return { type, depth, html, data, meta, error }; }); // State-machine to convert into flat list of sections. let title: string | undefined; let sections: IMarkdownSection[] = []; const addHtml = (section: IMarkdownSection, html: string) => `${section.html || ''}\n${html || ''}`.trim(); parts.forEach(part => { const isHeading = part.type === 'heading'; if (isHeading || sections.length === 0) { const depth = part.depth || 0; sections = [...sections, { index: -1, depth, data: [] }]; } const section = { ...sections[sections.length - 1] }; if (isHeading) { section.titleHtml = part.html; section.title = stripTags(part.html); } if (part.data) { const { meta, data: value } = part; section.data = [...section.data, { meta, value }]; } if (!part.data && !isHeading) { section.html = addHtml(section, part.html); } if (part.data && part.meta && part.meta.includes('RENDER')) { section.html = addHtml(section, part.html); } sections = [ ...sections.slice(0, sections.length - 1), valueUtil.deleteEmpty(section), ]; if (!title && isHeading && part.depth === 1) { title = stripTags(part.html); } }); // Store data. sections = sections.map((s, index) => ({ ...s, index })); result.data = { title, sections }; } catch (err) { let message = `Failed to parse markdown.`; message = `${message} ${err.message}`; result.error = { message }; } return valueUtil.deleteUndefined(result); } /** * HELPERS */ function stripTags(text: string) { return text .trim() .replace(/^\<[a-z1-9]+\>/, '') .replace(/\<\/[a-z1-9]+\>$/, ''); }