UNPKG

@diplodoc/transform

Version:

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

192 lines (156 loc) 5.76 kB
import {bold} from 'chalk'; import GithubSlugger from 'github-slugger'; import StateCore from 'markdown-it/lib/rules_core/state_core'; import Token from 'markdown-it/lib/token'; import {escapeHtml} from 'markdown-it/lib/common/utils'; import slugify from 'slugify'; import {headingInfo} from '../../utils'; import {MarkdownItPluginCb} from '../typings'; import {CUSTOM_ID_EXCEPTION, CUSTOM_ID_REGEXP} from './constants'; function createLinkTokens( state: StateCore, id: string, title: string, setId = false, href: string, ) { const open = new state.Token('link_open', 'a', 1); const close = new state.Token('link_close', 'a', -1); if (setId) { open.attrSet('id', id); } open.attrSet('href', href + '#' + id); open.attrSet('class', 'yfm-anchor'); open.attrSet('aria-hidden', 'true'); // SEO: render invisible heading title because link must have text content. const hiddenDesc = new state.Token('anchor_hidden_desc', '', 0); hiddenDesc.content = title; return [open, hiddenDesc, close]; } const getCustomIds = (content: string) => { const ids: string[] = []; content.replace(CUSTOM_ID_REGEXP, (match, customId) => { if (match !== CUSTOM_ID_EXCEPTION) { ids.push(customId); } return ''; }); return ids.length ? ids : null; }; const removeCustomId = (content: string) => { if (CUSTOM_ID_REGEXP.test(content)) { return content .replace(CUSTOM_ID_REGEXP, (match) => { if (match === CUSTOM_ID_EXCEPTION) { return match; } return ''; }) .trim(); } return content; }; const removeCustomIds = (token: Token) => { token.content = removeCustomId(token.content); token.children?.forEach((child) => { child.content = removeCustomId(child.content); }); }; interface Options { extractTitle?: boolean; supportGithubAnchors?: boolean; disableCommonAnchors?: boolean; transformLink: (v: string) => string; getPublicPath?: (options: Options, v?: string) => string; } const index: MarkdownItPluginCb<Options> = (md, options) => { const {extractTitle, path, log, supportGithubAnchors, getPublicPath, disableCommonAnchors} = options; const plugin = (state: StateCore) => { /* Do not use the plugin if it is included in the file */ if (state.env.includes && state.env.includes.length) { return; } const href = getPublicPath ? getPublicPath(options, state.env.path) : ''; const ids: Record<string, number> = {}; const tokens = state.tokens; let i = 0; const slugger = new GithubSlugger(); while (i < tokens.length) { const token = tokens[i]; const isHeading = token.type === 'heading_open'; if (isHeading) { const {title, level} = headingInfo(tokens, i); const inlineToken = tokens[i + 1]; let id = token.attrGet('id'); let ghId: string; if (!title) { log.warn(`Header without title${path ? ` in ${bold(path)}` : ''}`); } if (level < 2 && extractTitle) { // if there are any custom ids in the level 1 heading we should clear them removeCustomIds(tokens[i + 1]); i += 3; continue; } const customIds = getCustomIds(inlineToken.content); if (customIds) { id = customIds[0]; removeCustomIds(tokens[i + 1]); } else { id = slugify(title, { lower: true, remove: /[^\w\s$_\-,;=/]+/g, }); ghId = slugger.slug(title); } token.attrSet('id', id); if (ids[id]) { id = id + ids[id]++; token.attrSet('id', id); } else { ids[id] = 1; } const allAnchorIds = customIds ? customIds : [id]; const anchorTitle = removeCustomId(title).replace(/`/g, ''); allAnchorIds.forEach((customId) => { const setId = id !== customId; if (!disableCommonAnchors) { const linkTokens = createLinkTokens( state, customId, anchorTitle, setId, href, ); inlineToken.children?.unshift(...linkTokens); } if (supportGithubAnchors) { const ghLinkTokens = createLinkTokens(state, ghId, anchorTitle, true, href); inlineToken.children?.unshift(...ghLinkTokens); } }); i += 3; continue; } i++; } }; try { md.core.ruler.after('includes', 'anchors', plugin); } catch { try { md.core.ruler.after('curly_attributes', 'anchors', plugin); } catch { md.core.ruler.push('anchors', plugin); } } md.renderer.rules.anchor_hidden_desc = function (tokens, index) { return ( '<span class="visually-hidden" data-no-index="true">' + escapeHtml(tokens[index].content) + '</span>' ); }; }; export = index;