const CALLOUT_TYPES = new Set([ "aside", "caution", "note", "tip", "warning", ]); const CALLOUT_MARKERS = new Map([ ["📝", "note"], ["💡", "tip"], ["⚡", "tip"], ["⚠️", "warning"], ["⚠", "warning"], ]); const BRACKET_CALLOUT_PATTERN = /^\s*\[!(aside|caution|note|tip|warning)(?::\s*([^\]\r\n]+)|\|\s*([^\]\r\n]+))?\]\s*/i; function textFromInline(node) { if (!node) return ""; if (typeof node.value === "string") return node.value; if (!Array.isArray(node.children)) return ""; return node.children.map(textFromInline).join(""); } function normalizeType(value) { return value .trim() .replace(/^["“”]+|["“”]+$/g, "") .replace(/:$/, "") .toLowerCase(); } function calloutType(value) { const type = normalizeType(value); return CALLOUT_TYPES.has(type) ? type : null; } function parseBracketCallout(value) { const match = value.match(BRACKET_CALLOUT_PATTERN); if (!match) return null; return { kind: "bracket", type: calloutType(match[1]), title: (match[2] ?? match[3] ?? "").trim(), }; } function paragraphCallout(paragraph) { if (paragraph?.type !== "paragraph") return null; const first = paragraph.children?.[0]; if (first?.type === "text") { const bracket = parseBracketCallout(first.value); if (bracket?.type) { return bracket; } const trimmed = first.value.trimStart(); for (const [marker, type] of CALLOUT_MARKERS) { if (trimmed.startsWith(marker)) { return { kind: "marker", type, title: "", marker }; } } } if (first?.type !== "strong") return null; const type = calloutType(textFromInline(first)); if (!type) return null; return { kind: "strong", type, title: "" }; } function cleanupParagraphStart(paragraph) { while (paragraph.children?.length > 0) { const first = paragraph.children[0]; if (first.type === "text") { first.value = first.value.replace(/^\s+/, ""); if (first.value) return; paragraph.children.shift(); continue; } if (first.type === "break") { paragraph.children.shift(); continue; } return; } } function calloutTitleParagraph(title) { return { type: "paragraph", data: { hName: "p", hProperties: { className: ["callout-title"], }, }, children: [{ type: "text", value: title }], }; } function removeLeadingCallout(paragraph, callout) { if (paragraph?.type !== "paragraph") return; const first = paragraph.children?.[0]; if (callout.kind === "bracket" && first?.type === "text") { first.value = first.value.replace(BRACKET_CALLOUT_PATTERN, ""); cleanupParagraphStart(paragraph); return; } if (callout.kind === "marker" && first?.type === "text") { const escapedMarker = callout.marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); first.value = first.value.replace(new RegExp(`^\\s*${escapedMarker}\\s*`, "u"), ""); cleanupParagraphStart(paragraph); return; } const firstText = calloutType(textFromInline(first)); if (callout.kind !== "strong" || !first || first.type !== "strong" || firstText !== callout.type) { return; } paragraph.children.shift(); const next = paragraph.children[0]; if (next?.type === "text") { next.value = next.value.replace(/^:\s*/, "").replace(/^\s+/, ""); if (!next.value) paragraph.children.shift(); } cleanupParagraphStart(paragraph); } function transformBlockquote(node) { if (node.type !== "blockquote") return; const first = node.children?.[0]; const callout = paragraphCallout(first); if (!callout) return; removeLeadingCallout(first, callout); if (first.children?.length === 0) { node.children.shift(); } if (callout.title) { node.children.unshift(calloutTitleParagraph(callout.title)); } node.data = { hName: "aside", hProperties: { className: ["callout", `callout-${callout.type}`], }, }; } function visit(node) { transformBlockquote(node); if (!Array.isArray(node.children)) return; for (const child of node.children) { visit(child); } } export default function remarkCallouts() { return function transformer(tree) { visit(tree); }; }