UNPKG

@diplodoc/transform

Version:

A simple transformer of text in YFM (Yandex Flavored Markdown) to HTML

170 lines (141 loc) 4.99 kB
import Core from 'markdown-it/lib/parser_core'; import Token from 'markdown-it/lib/token'; import {bold} from 'chalk'; import yaml from 'js-yaml'; import StateCore from 'markdown-it/lib/rules_core/state_core'; import {MarkdownItPluginCb} from '../typings'; interface Options { extractChangelogs?: boolean; } const CHANGELOG_OPEN_RE = /^\{% changelog %}/; const CHANGELOG_CLOSE_RE = /^\{% endchangelog %}/; function isOpenToken(tokens: Token[], i: number) { return ( tokens[i].type === 'paragraph_open' && tokens[i + 1].type === 'inline' && tokens[i + 2].type === 'paragraph_close' && CHANGELOG_OPEN_RE.test(tokens[i + 1].content) ); } function isCloseToken(tokens: Token[], i: number) { return ( tokens[i]?.type === 'paragraph_open' && tokens[i + 1].type === 'inline' && tokens[i + 2].type === 'paragraph_close' && CHANGELOG_CLOSE_RE.test(tokens[i + 1].content) ); } function isTitle(tokens: Token[], i = 0) { return ( tokens[i].type === 'heading_open' && tokens[i + 1].type === 'inline' && tokens[i + 2].type === 'heading_close' ); } function isImageParagraph(tokens: Token[], i = 0) { return ( tokens[i].type === 'paragraph_open' && tokens[i + 1].type === 'inline' && tokens[i + 2].type === 'paragraph_close' && tokens[i + 1].children?.some((t) => t.type === 'image') ); } function parseBody(tokens: Token[], state: StateCore) { const {md, env} = state; const metadataToken = tokens.shift(); if (metadataToken?.type !== 'fence') { throw new Error('Metadata tag not found'); } let metadata: Record<string, unknown> = {}; const rawMetadata = yaml.load(metadataToken.content, { schema: yaml.JSON_SCHEMA, }) as Record<string, unknown>; if (rawMetadata && typeof rawMetadata === 'object') { metadata = rawMetadata; } if (!isTitle(tokens)) { throw new Error('Title tag not found'); } const title = tokens.splice(0, 3)[1].content; let image; if (isImageParagraph(tokens)) { const paragraphTokens = tokens.splice(0, 3); const imageToken = paragraphTokens[1]?.children?.find((token) => token.type === 'image'); if (imageToken) { const width = Number(imageToken.attrGet('width')); const height = Number(imageToken.attrGet('height')); let ratio; if (Number.isFinite(width) && Number.isFinite(height)) { ratio = height / width; } let alt = imageToken.attrGet('title') || ''; if (!alt && imageToken.children) { alt = md.renderer.renderInlineAsText(imageToken.children, md.options, env); } image = { src: imageToken.attrGet('src'), alt, ratio, }; } } const description = md.renderer.render(tokens, md.options, env); if (typeof metadata.storyId === 'number') { metadata.storyId = String(metadata.storyId); } return { ...metadata, title, image, description, }; } const changelog: MarkdownItPluginCb<Options> = function (md, {extractChangelogs, log, path}) { const plugin: Core.RuleCore = (state) => { const {tokens, env} = state; for (let i = 0, len = tokens.length; i < len; i++) { const isOpen = isOpenToken(tokens, i); if (!isOpen) continue; const openAt = i; let isCloseFound = false; while (i < len) { i++; if (isCloseToken(tokens, i)) { isCloseFound = true; break; } } if (!isCloseFound) { log.error(`Changelog close tag in not found: ${bold(path)}`); break; } const closeAt = i + 2; if (env && extractChangelogs) { const content = tokens.slice(openAt, closeAt + 1); // cut open content.splice(0, 3); // cut close content.splice(-3); try { const changelogLocal = parseBody(content, state); if (!env.changelogs) { env.changelogs = []; } env.changelogs.push(changelogLocal); } catch (err) { log.error(`Changelog error: ${(err as Error).message} in ${bold(path)}`); continue; } } tokens.splice(openAt, closeAt + 1 - openAt); len = tokens.length; i = openAt - 1; } }; try { md.core.ruler.before('curly_attributes', 'changelog', plugin); } catch (e) { md.core.ruler.push('changelog', plugin); } }; export = changelog;