diff --git a/markdown/m2h/handlers/index.ts b/markdown/m2h/handlers/index.ts index 1f37a80e71d3..54551bdf5f3b 100644 --- a/markdown/m2h/handlers/index.ts +++ b/markdown/m2h/handlers/index.ts @@ -6,8 +6,9 @@ import { asDefinitionList, isDefinitionList } from "./dl.js"; import { Handler, Handlers, State } from "mdast-util-to-hast"; /* A utilitary function which parses a JSON gettext file - to return a Map with each localized string and its matching ID */ -function getL10nCardMap(locale = DEFAULT_LOCALE) { + to return a Map with each key and its corresponding localized string. + If the locale specified is not found, it will fallback to English */ +function getL10nCardMap(locale = DEFAULT_LOCALE): Map { // Test if target localization file exists, if // not, fallback on English let localeFilePath = new URL( @@ -27,35 +28,61 @@ function getL10nCardMap(locale = DEFAULT_LOCALE) { Object.keys(listMsgObj).forEach((msgName) => { l10nCardMap.set( - listMsgObj[msgName]["msgstr"][0], - listMsgObj[msgName]["msgid"] + listMsgObj[msgName]["msgid"], + listMsgObj[msgName]["msgstr"][0] ); }); return l10nCardMap; } -function getNotecardType(node, locale) { +interface NotecardType { + type: string; + isGFM: boolean; + magicKeyword: string; +} + +function getNotecardType(node: any, locale: string): NotecardType | null { + const types = ["note", "warning", "callout"]; + if (!node.children) { return null; } const [child] = node.children; - if (!child || !child.children) { + if (!child?.children) { return null; } const [grandChild] = child.children; - if (grandChild.type != "strong" || !grandChild.children) { - return null; - } - // E.g. in en-US magicKeyword === Note: - const magicKeyword = grandChild.children[0].value; const l10nCardMap = getL10nCardMap(locale); - let type; - if (l10nCardMap.has(magicKeyword)) { - const msgId = l10nCardMap.get(magicKeyword); - type = msgId.split("_")[1]; + + // GFM proposed notecard syntax -- https://github.com/orgs/community/discussions/16925 + if (grandChild.type === "text") { + const firstLine = grandChild.value.split("\n")[0]; + const match = firstLine.match( + new RegExp(`\\[!(${types.map((t) => t.toUpperCase()).join("|")})\\]`) + ); + if (match) { + const type = match[1].toLowerCase(); + const magicKeyword = l10nCardMap.get(`card_${type}_label`); + return { type, isGFM: true, magicKeyword }; + } + } + + if (grandChild.type === "strong" && grandChild.children) { + // E.g. in en-US magicKeyword === Note: + const magicKeyword = grandChild.children[0].value; + + for (const entry of l10nCardMap.entries()) { + if (entry[1] === magicKeyword) { + const type = entry[0].split("_")[1]; + return types.includes(type) + ? { type, isGFM: false, magicKeyword } + : null; + } + } } - return type == "warning" || type == "note" || type == "callout" ? type : null; + + return null; } export function buildLocalizedHandlers(locale: string): Handlers { @@ -86,18 +113,45 @@ export function buildLocalizedHandlers(locale: string): Handlers { blockquote(state: State, node: any): ReturnType { const type = getNotecardType(node, locale); if (type) { - const isCallout = type == "callout"; - if (isCallout) { - if (node.children[0].children.length <= 1) { - node.children.splice(0, 1); - } else { - node.children[0].children.splice(0, 1); + const isCallout = type.type == "callout"; + + if (type.isGFM) { + // Handle GFM proposed syntax + node.children[0].children[0].value = + node.children[0].children[0].value.replace(/\[!\w+\]\n/, ""); + + // If the type isn't a callout, add the magic keyword + if (!isCallout) { + node.children[0].children.unshift({ + type: "strong", + children: [ + { + type: "text", + value: type.magicKeyword, + }, + ], + }); + node.children[0].children[1].value = + (["zh-CN", "zh-TW"].includes(locale) ? "" : " ") + + node.children[0].children[1].value; + } + } else { + // Remove "Callout:" text + if (isCallout) { + if (node.children[0].children.length <= 1) { + node.children.splice(0, 1); + } else { + node.children[0].children.splice(0, 1); + } } } + return { type: "element", tagName: "div", - properties: { className: isCallout ? [type] : ["notecard", type] }, + properties: { + className: isCallout ? [type.type] : ["notecard", type.type], + }, children: state.wrap(state.all(node), true), }; }