blogs/sites/research/src/remark/callouts.mjs
Neeldhara Misra b03d27cc3a
Some checks are pending
Build / build (push) Waiting to run
Make callout headings explicit
2026-06-26 01:44:41 +05:30

178 lines
4.1 KiB
JavaScript

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);
};
}