This commit is contained in:
parent
0025e453e8
commit
b03d27cc3a
20 changed files with 1052 additions and 330 deletions
|
|
@ -94,7 +94,9 @@ forward, use normal Markdown and let Astro CSS handle presentation.
|
|||
|
||||
### Callouts
|
||||
|
||||
Write callouts as portable blockquotes. This is the canonical form:
|
||||
Write callouts as portable blockquotes. The type controls the visual styling,
|
||||
but it does not create a visible heading by itself. This is the canonical form
|
||||
for a titleless note:
|
||||
|
||||
```markdown
|
||||
> **Note**
|
||||
|
|
@ -110,13 +112,31 @@ Supported labels are `Note`, `Tip`, `Warning`, `Caution`, and `Aside`:
|
|||
> This point needs attention.
|
||||
```
|
||||
|
||||
Obsidian-style labels also work:
|
||||
Obsidian-style bracket labels also work. A plain bracket label is still
|
||||
titleless:
|
||||
|
||||
```markdown
|
||||
> [!tip]
|
||||
> This is a tip.
|
||||
```
|
||||
|
||||
To show a heading, put it explicitly inside the bracket after `:` or `|`:
|
||||
|
||||
```markdown
|
||||
> [!tip: Useful observation]
|
||||
>
|
||||
> Text of the callout.
|
||||
|
||||
> [!warning|Check this assumption]
|
||||
>
|
||||
> This point needs attention.
|
||||
```
|
||||
|
||||
The text after `:` or `|` is treated as the only callout heading. Do not write
|
||||
`> [!tip] Useful observation` for a heading; that form is interpreted as a
|
||||
titleless tip whose body begins with “Useful observation”. This keeps the
|
||||
Markdown unambiguous across Astro, Pandoc, and LaTeX.
|
||||
|
||||
For quick writing, a few leading emoji markers are recognized and stripped:
|
||||
|
||||
```markdown
|
||||
|
|
@ -139,9 +159,9 @@ aside -> blogasidebox
|
|||
|
||||
The LaTeX boxes use `tcolorbox` with the `breakable` and `enhanced` libraries,
|
||||
a light tinted background, a thin frame, a stronger left rule, and a small
|
||||
attached title label. This follows the same design vocabulary as the DM book
|
||||
callouts: compact colored boxes that can split across pages without becoming
|
||||
visually heavy.
|
||||
attached title only when the Markdown explicitly provides one. This follows the
|
||||
same design vocabulary as the DM book callouts: compact colored boxes that can
|
||||
split across pages without becoming visually heavy.
|
||||
|
||||
Quarto margin notes such as `[text]{.aside}` were converted to:
|
||||
|
||||
|
|
|
|||
|
|
@ -279,7 +279,8 @@ authorName: "Neeldhara"
|
|||
- Standard Markdown headings, lists, links, tables, code fences, footnotes, and raw HTML are preserved.
|
||||
- Use colocated relative assets: \`\`.
|
||||
- Quarto image attributes such as \`{width=70%}\` are removed during import. Use normal Markdown now; layout should be handled by Astro CSS.
|
||||
- Quarto callouts are converted to simple blockquotes:
|
||||
- Quarto callouts are converted to simple blockquotes. The callout type controls
|
||||
styling only; it does not create a visible heading by default:
|
||||
|
||||
\`\`\`markdown
|
||||
> **Note**
|
||||
|
|
@ -287,14 +288,22 @@ authorName: "Neeldhara"
|
|||
> Text of the callout.
|
||||
\`\`\`
|
||||
|
||||
- For new notes, use the same blockquote convention for portable callouts:
|
||||
- For new notes, the bracket form is also supported:
|
||||
|
||||
\`\`\`markdown
|
||||
> **Warning**
|
||||
> [!warning]
|
||||
>
|
||||
> This point needs attention.
|
||||
\`\`\`
|
||||
|
||||
- To show a heading, put it explicitly inside the bracket after \`:\` or \`|\`:
|
||||
|
||||
\`\`\`markdown
|
||||
> [!tip: Useful observation]
|
||||
>
|
||||
> Text of the callout.
|
||||
\`\`\`
|
||||
|
||||
- Quarto margin notes such as \`[text]{.aside}\` are converted to:
|
||||
|
||||
\`\`\`markdown
|
||||
|
|
|
|||
|
|
@ -101,6 +101,13 @@ local function first_inline_text(block)
|
|||
return inline_text(block.content[1])
|
||||
end
|
||||
|
||||
local function paragraph_text(block)
|
||||
if not block or block.t ~= "Para" then
|
||||
return ""
|
||||
end
|
||||
return pandoc.utils.stringify(block)
|
||||
end
|
||||
|
||||
local function latex_escape(value)
|
||||
value = tostring(value or "")
|
||||
value = value:gsub("\\", "\\textbackslash{}")
|
||||
|
|
@ -356,6 +363,70 @@ local function strip_text_prefix(block, pattern)
|
|||
end
|
||||
end
|
||||
|
||||
local function cleanup_inline_start(block)
|
||||
if not block or block.t ~= "Para" or not block.content then
|
||||
return
|
||||
end
|
||||
|
||||
while #block.content > 0 do
|
||||
local first = block.content[1]
|
||||
if first.t == "Space" or first.t == "SoftBreak" or first.t == "LineBreak" then
|
||||
table.remove(block.content, 1)
|
||||
elseif first.t == "Str" then
|
||||
first.text = first.text:gsub("^%s+", "")
|
||||
if first.text == "" then
|
||||
table.remove(block.content, 1)
|
||||
else
|
||||
return
|
||||
end
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function strip_bracket_callout(block)
|
||||
if not block or block.t ~= "Para" or not block.content then
|
||||
return
|
||||
end
|
||||
|
||||
while #block.content > 0 do
|
||||
local first = block.content[1]
|
||||
local text = inline_text(first)
|
||||
local close_index = text:find("%]")
|
||||
|
||||
if close_index then
|
||||
local remaining = text:sub(close_index + 1):gsub("^%s+", "")
|
||||
if first.t == "Str" and remaining ~= "" then
|
||||
first.text = remaining
|
||||
elseif remaining ~= "" then
|
||||
block.content[1] = pandoc.Str(remaining)
|
||||
else
|
||||
table.remove(block.content, 1)
|
||||
end
|
||||
break
|
||||
end
|
||||
|
||||
table.remove(block.content, 1)
|
||||
end
|
||||
|
||||
cleanup_inline_start(block)
|
||||
end
|
||||
|
||||
local function parse_bracket_callout(text)
|
||||
local key, title = text:match("^%s*%[!([%a-]+)%s*[:|]%s*([^%]]+)%]")
|
||||
if key then
|
||||
return normalize_label(key), title:gsub("^%s+", ""):gsub("%s+$", "")
|
||||
end
|
||||
|
||||
key = text:match("^%s*%[!([%a-]+)%]")
|
||||
if key then
|
||||
return normalize_label(key), ""
|
||||
end
|
||||
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local function detect_callout(block)
|
||||
local text = first_inline_text(block)
|
||||
if text == "" then
|
||||
|
|
@ -365,21 +436,18 @@ local function detect_callout(block)
|
|||
if block.content[1].t == "Strong" then
|
||||
local key = normalize_label(text)
|
||||
if callout_labels[key] then
|
||||
return { key = key, label = callout_labels[key], remover = "strong" }
|
||||
return { key = key, title = "", remover = "strong" }
|
||||
end
|
||||
end
|
||||
|
||||
local bracket = text:match("^%[!([%a-]+)%]")
|
||||
if bracket then
|
||||
local key = normalize_label(bracket)
|
||||
if callout_labels[key] then
|
||||
return { key = key, label = callout_labels[key], remover = "bracket" }
|
||||
end
|
||||
local key, title = parse_bracket_callout(paragraph_text(block))
|
||||
if key and callout_labels[key] then
|
||||
return { key = key, title = title or "", remover = "bracket" }
|
||||
end
|
||||
|
||||
for _, item in ipairs(callout_markers) do
|
||||
if text:sub(1, #item.marker) == item.marker then
|
||||
return { key = item.key, label = callout_labels[item.key], remover = "marker", marker = item.marker }
|
||||
return { key = item.key, title = "", remover = "marker", marker = item.marker }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -400,7 +468,7 @@ local function callout_to_latex(el)
|
|||
if callout.remover == "strong" then
|
||||
remove_first_inline(blocks[1])
|
||||
elseif callout.remover == "bracket" then
|
||||
strip_text_prefix(blocks[1], "^%[![%a-]+%]%s*")
|
||||
strip_bracket_callout(blocks[1])
|
||||
elseif callout.remover == "marker" then
|
||||
strip_text_prefix(blocks[1], "^" .. callout.marker .. "%s*")
|
||||
end
|
||||
|
|
@ -412,7 +480,7 @@ local function callout_to_latex(el)
|
|||
local env = callout_envs[callout.key] or "blognotebox"
|
||||
return pandoc.RawBlock(
|
||||
"latex",
|
||||
"\\begin{" .. env .. "}{}\n"
|
||||
"\\begin{" .. env .. "}{" .. latex_escape(callout.title or "") .. "}\n"
|
||||
.. blocks_to_latex(blocks)
|
||||
.. "\n\\end{" .. env .. "}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -46,49 +46,119 @@
|
|||
}
|
||||
}
|
||||
|
||||
\newtcolorbox{blognotebox}[2][]{
|
||||
\NewDocumentEnvironment{blognotebox}{O{} m}{%
|
||||
\if\relax\detokenize{#2}\relax
|
||||
\begin{tcolorbox}[
|
||||
blog callout,
|
||||
title={NOTE\if\relax\detokenize{#2}\relax\else: #2\fi},
|
||||
colback=BlogCalloutNoteBack,
|
||||
colframe=BlogCalloutNoteFrame,
|
||||
boxed title style={colback=BlogCalloutNoteBack},
|
||||
#1
|
||||
]%
|
||||
\else
|
||||
\begin{tcolorbox}[
|
||||
blog callout,
|
||||
title={#2},
|
||||
colback=BlogCalloutNoteBack,
|
||||
colframe=BlogCalloutNoteFrame,
|
||||
boxed title style={colback=BlogCalloutNoteBack},
|
||||
#1
|
||||
]%
|
||||
\fi
|
||||
}{%
|
||||
\end{tcolorbox}%
|
||||
}
|
||||
|
||||
\newtcolorbox{blogtipbox}[2][]{
|
||||
\NewDocumentEnvironment{blogtipbox}{O{} m}{%
|
||||
\if\relax\detokenize{#2}\relax
|
||||
\begin{tcolorbox}[
|
||||
blog callout,
|
||||
title={TIP\if\relax\detokenize{#2}\relax\else: #2\fi},
|
||||
colback=BlogCalloutTipBack,
|
||||
colframe=BlogCalloutTipFrame,
|
||||
boxed title style={colback=BlogCalloutTipBack},
|
||||
#1
|
||||
]%
|
||||
\else
|
||||
\begin{tcolorbox}[
|
||||
blog callout,
|
||||
title={#2},
|
||||
colback=BlogCalloutTipBack,
|
||||
colframe=BlogCalloutTipFrame,
|
||||
boxed title style={colback=BlogCalloutTipBack},
|
||||
#1
|
||||
]%
|
||||
\fi
|
||||
}{%
|
||||
\end{tcolorbox}%
|
||||
}
|
||||
|
||||
\newtcolorbox{blogwarningbox}[2][]{
|
||||
\NewDocumentEnvironment{blogwarningbox}{O{} m}{%
|
||||
\if\relax\detokenize{#2}\relax
|
||||
\begin{tcolorbox}[
|
||||
blog callout,
|
||||
title={WARNING\if\relax\detokenize{#2}\relax\else: #2\fi},
|
||||
colback=BlogCalloutWarningBack,
|
||||
colframe=BlogCalloutWarningFrame,
|
||||
boxed title style={colback=BlogCalloutWarningBack},
|
||||
#1
|
||||
]%
|
||||
\else
|
||||
\begin{tcolorbox}[
|
||||
blog callout,
|
||||
title={#2},
|
||||
colback=BlogCalloutWarningBack,
|
||||
colframe=BlogCalloutWarningFrame,
|
||||
boxed title style={colback=BlogCalloutWarningBack},
|
||||
#1
|
||||
]%
|
||||
\fi
|
||||
}{%
|
||||
\end{tcolorbox}%
|
||||
}
|
||||
|
||||
\newtcolorbox{blogcautionbox}[2][]{
|
||||
\NewDocumentEnvironment{blogcautionbox}{O{} m}{%
|
||||
\if\relax\detokenize{#2}\relax
|
||||
\begin{tcolorbox}[
|
||||
blog callout,
|
||||
title={CAUTION\if\relax\detokenize{#2}\relax\else: #2\fi},
|
||||
colback=BlogCalloutCautionBack,
|
||||
colframe=BlogCalloutCautionFrame,
|
||||
boxed title style={colback=BlogCalloutCautionBack},
|
||||
#1
|
||||
]%
|
||||
\else
|
||||
\begin{tcolorbox}[
|
||||
blog callout,
|
||||
title={#2},
|
||||
colback=BlogCalloutCautionBack,
|
||||
colframe=BlogCalloutCautionFrame,
|
||||
boxed title style={colback=BlogCalloutCautionBack},
|
||||
#1
|
||||
]%
|
||||
\fi
|
||||
}{%
|
||||
\end{tcolorbox}%
|
||||
}
|
||||
|
||||
\newtcolorbox{blogasidebox}[2][]{
|
||||
\NewDocumentEnvironment{blogasidebox}{O{} m}{%
|
||||
\if\relax\detokenize{#2}\relax
|
||||
\begin{tcolorbox}[
|
||||
blog callout,
|
||||
title={ASIDE\if\relax\detokenize{#2}\relax\else: #2\fi},
|
||||
colback=BlogCalloutAsideBack,
|
||||
colframe=BlogCalloutAsideFrame,
|
||||
boxed title style={colback=BlogCalloutAsideBack},
|
||||
#1
|
||||
]%
|
||||
\else
|
||||
\begin{tcolorbox}[
|
||||
blog callout,
|
||||
title={#2},
|
||||
colback=BlogCalloutAsideBack,
|
||||
colframe=BlogCalloutAsideFrame,
|
||||
boxed title style={colback=BlogCalloutAsideBack},
|
||||
#1
|
||||
]%
|
||||
\fi
|
||||
}{%
|
||||
\end{tcolorbox}%
|
||||
}
|
||||
|
||||
\ExplSyntaxOn
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
const CALLOUT_LABELS = new Map([
|
||||
["aside", "Aside"],
|
||||
["caution", "Caution"],
|
||||
["note", "Note"],
|
||||
["tip", "Tip"],
|
||||
["warning", "Warning"],
|
||||
const CALLOUT_TYPES = new Set([
|
||||
"aside",
|
||||
"caution",
|
||||
"note",
|
||||
"tip",
|
||||
"warning",
|
||||
]);
|
||||
|
||||
const CALLOUT_MARKERS = new Map([
|
||||
["📝", "Note"],
|
||||
["💡", "Tip"],
|
||||
["⚡", "Tip"],
|
||||
["⚠️", "Warning"],
|
||||
["⚠", "Warning"],
|
||||
["📝", "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;
|
||||
|
|
@ -21,7 +23,7 @@ function textFromInline(node) {
|
|||
return node.children.map(textFromInline).join("");
|
||||
}
|
||||
|
||||
function normalizeLabel(value) {
|
||||
function normalizeType(value) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^["“”]+|["“”]+$/g, "")
|
||||
|
|
@ -29,48 +31,99 @@ function normalizeLabel(value) {
|
|||
.toLowerCase();
|
||||
}
|
||||
|
||||
function paragraphCalloutLabel(paragraph) {
|
||||
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 match = first.value.match(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i);
|
||||
if (match) {
|
||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
||||
const bracket = parseBracketCallout(first.value);
|
||||
if (bracket?.type) {
|
||||
return bracket;
|
||||
}
|
||||
|
||||
const trimmed = first.value.trimStart();
|
||||
for (const [marker, label] of CALLOUT_MARKERS) {
|
||||
for (const [marker, type] of CALLOUT_MARKERS) {
|
||||
if (trimmed.startsWith(marker)) {
|
||||
return label;
|
||||
return { kind: "marker", type, title: "", marker };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first?.type !== "strong") return null;
|
||||
|
||||
const label = CALLOUT_LABELS.get(normalizeLabel(textFromInline(first)));
|
||||
if (!label) return null;
|
||||
const type = calloutType(textFromInline(first));
|
||||
if (!type) return null;
|
||||
|
||||
return label;
|
||||
return { kind: "strong", type, title: "" };
|
||||
}
|
||||
|
||||
function removeLeadingLabel(paragraph, label) {
|
||||
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 (first?.type === "text") {
|
||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
||||
for (const marker of CALLOUT_MARKERS.keys()) {
|
||||
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
first.value = first.value.replace(new RegExp(`^\\s*${escapedMarker}\\s*`, "u"), "");
|
||||
}
|
||||
if (!first.value) paragraph.children.shift();
|
||||
if (callout.kind === "bracket" && first?.type === "text") {
|
||||
first.value = first.value.replace(BRACKET_CALLOUT_PATTERN, "");
|
||||
cleanupParagraphStart(paragraph);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstText = normalizeLabel(textFromInline(first));
|
||||
if (!first || first.type !== "strong" || firstText !== label.toLowerCase()) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -81,25 +134,30 @@ function removeLeadingLabel(paragraph, label) {
|
|||
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 label = paragraphCalloutLabel(first);
|
||||
if (!label) return;
|
||||
const callout = paragraphCallout(first);
|
||||
if (!callout) return;
|
||||
|
||||
removeLeadingLabel(first, label);
|
||||
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-${label.toLowerCase()}`],
|
||||
dataCalloutLabel: label,
|
||||
className: ["callout", `callout-${callout.type}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,6 +221,15 @@
|
|||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.article-content :where(.callout-title) {
|
||||
margin: 0 0 0.45rem;
|
||||
color: color-mix(in srgb, hsl(var(--primary)) 78%, hsl(var(--foreground)));
|
||||
font-family: var(--font-fraunces);
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.article-content :where(.callout h1) {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
const CALLOUT_LABELS = new Map([
|
||||
["aside", "Aside"],
|
||||
["caution", "Caution"],
|
||||
["note", "Note"],
|
||||
["tip", "Tip"],
|
||||
["warning", "Warning"],
|
||||
const CALLOUT_TYPES = new Set([
|
||||
"aside",
|
||||
"caution",
|
||||
"note",
|
||||
"tip",
|
||||
"warning",
|
||||
]);
|
||||
|
||||
const CALLOUT_MARKERS = new Map([
|
||||
["📝", "Note"],
|
||||
["💡", "Tip"],
|
||||
["⚡", "Tip"],
|
||||
["⚠️", "Warning"],
|
||||
["⚠", "Warning"],
|
||||
["📝", "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;
|
||||
|
|
@ -21,7 +23,7 @@ function textFromInline(node) {
|
|||
return node.children.map(textFromInline).join("");
|
||||
}
|
||||
|
||||
function normalizeLabel(value) {
|
||||
function normalizeType(value) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^["“”]+|["“”]+$/g, "")
|
||||
|
|
@ -29,48 +31,99 @@ function normalizeLabel(value) {
|
|||
.toLowerCase();
|
||||
}
|
||||
|
||||
function paragraphCalloutLabel(paragraph) {
|
||||
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 match = first.value.match(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i);
|
||||
if (match) {
|
||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
||||
const bracket = parseBracketCallout(first.value);
|
||||
if (bracket?.type) {
|
||||
return bracket;
|
||||
}
|
||||
|
||||
const trimmed = first.value.trimStart();
|
||||
for (const [marker, label] of CALLOUT_MARKERS) {
|
||||
for (const [marker, type] of CALLOUT_MARKERS) {
|
||||
if (trimmed.startsWith(marker)) {
|
||||
return label;
|
||||
return { kind: "marker", type, title: "", marker };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first?.type !== "strong") return null;
|
||||
|
||||
const label = CALLOUT_LABELS.get(normalizeLabel(textFromInline(first)));
|
||||
if (!label) return null;
|
||||
const type = calloutType(textFromInline(first));
|
||||
if (!type) return null;
|
||||
|
||||
return label;
|
||||
return { kind: "strong", type, title: "" };
|
||||
}
|
||||
|
||||
function removeLeadingLabel(paragraph, label) {
|
||||
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 (first?.type === "text") {
|
||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
||||
for (const marker of CALLOUT_MARKERS.keys()) {
|
||||
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
first.value = first.value.replace(new RegExp(`^\\s*${escapedMarker}\\s*`, "u"), "");
|
||||
}
|
||||
if (!first.value) paragraph.children.shift();
|
||||
if (callout.kind === "bracket" && first?.type === "text") {
|
||||
first.value = first.value.replace(BRACKET_CALLOUT_PATTERN, "");
|
||||
cleanupParagraphStart(paragraph);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstText = normalizeLabel(textFromInline(first));
|
||||
if (!first || first.type !== "strong" || firstText !== label.toLowerCase()) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -81,25 +134,30 @@ function removeLeadingLabel(paragraph, label) {
|
|||
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 label = paragraphCalloutLabel(first);
|
||||
if (!label) return;
|
||||
const callout = paragraphCallout(first);
|
||||
if (!callout) return;
|
||||
|
||||
removeLeadingLabel(first, label);
|
||||
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-${label.toLowerCase()}`],
|
||||
dataCalloutLabel: label,
|
||||
className: ["callout", `callout-${callout.type}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,6 +221,15 @@
|
|||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.article-content :where(.callout-title) {
|
||||
margin: 0 0 0.45rem;
|
||||
color: color-mix(in srgb, hsl(var(--primary)) 78%, hsl(var(--foreground)));
|
||||
font-family: var(--font-fraunces);
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.article-content :where(.callout h1) {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
const CALLOUT_LABELS = new Map([
|
||||
["aside", "Aside"],
|
||||
["caution", "Caution"],
|
||||
["note", "Note"],
|
||||
["tip", "Tip"],
|
||||
["warning", "Warning"],
|
||||
const CALLOUT_TYPES = new Set([
|
||||
"aside",
|
||||
"caution",
|
||||
"note",
|
||||
"tip",
|
||||
"warning",
|
||||
]);
|
||||
|
||||
const CALLOUT_MARKERS = new Map([
|
||||
["📝", "Note"],
|
||||
["💡", "Tip"],
|
||||
["⚡", "Tip"],
|
||||
["⚠️", "Warning"],
|
||||
["⚠", "Warning"],
|
||||
["📝", "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;
|
||||
|
|
@ -21,7 +23,7 @@ function textFromInline(node) {
|
|||
return node.children.map(textFromInline).join("");
|
||||
}
|
||||
|
||||
function normalizeLabel(value) {
|
||||
function normalizeType(value) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^["“”]+|["“”]+$/g, "")
|
||||
|
|
@ -29,48 +31,99 @@ function normalizeLabel(value) {
|
|||
.toLowerCase();
|
||||
}
|
||||
|
||||
function paragraphCalloutLabel(paragraph) {
|
||||
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 match = first.value.match(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i);
|
||||
if (match) {
|
||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
||||
const bracket = parseBracketCallout(first.value);
|
||||
if (bracket?.type) {
|
||||
return bracket;
|
||||
}
|
||||
|
||||
const trimmed = first.value.trimStart();
|
||||
for (const [marker, label] of CALLOUT_MARKERS) {
|
||||
for (const [marker, type] of CALLOUT_MARKERS) {
|
||||
if (trimmed.startsWith(marker)) {
|
||||
return label;
|
||||
return { kind: "marker", type, title: "", marker };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first?.type !== "strong") return null;
|
||||
|
||||
const label = CALLOUT_LABELS.get(normalizeLabel(textFromInline(first)));
|
||||
if (!label) return null;
|
||||
const type = calloutType(textFromInline(first));
|
||||
if (!type) return null;
|
||||
|
||||
return label;
|
||||
return { kind: "strong", type, title: "" };
|
||||
}
|
||||
|
||||
function removeLeadingLabel(paragraph, label) {
|
||||
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 (first?.type === "text") {
|
||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
||||
for (const marker of CALLOUT_MARKERS.keys()) {
|
||||
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
first.value = first.value.replace(new RegExp(`^\\s*${escapedMarker}\\s*`, "u"), "");
|
||||
}
|
||||
if (!first.value) paragraph.children.shift();
|
||||
if (callout.kind === "bracket" && first?.type === "text") {
|
||||
first.value = first.value.replace(BRACKET_CALLOUT_PATTERN, "");
|
||||
cleanupParagraphStart(paragraph);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstText = normalizeLabel(textFromInline(first));
|
||||
if (!first || first.type !== "strong" || firstText !== label.toLowerCase()) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -81,25 +134,30 @@ function removeLeadingLabel(paragraph, label) {
|
|||
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 label = paragraphCalloutLabel(first);
|
||||
if (!label) return;
|
||||
const callout = paragraphCallout(first);
|
||||
if (!callout) return;
|
||||
|
||||
removeLeadingLabel(first, label);
|
||||
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-${label.toLowerCase()}`],
|
||||
dataCalloutLabel: label,
|
||||
className: ["callout", `callout-${callout.type}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,6 +221,15 @@
|
|||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.article-content :where(.callout-title) {
|
||||
margin: 0 0 0.45rem;
|
||||
color: color-mix(in srgb, hsl(var(--primary)) 78%, hsl(var(--foreground)));
|
||||
font-family: var(--font-fraunces);
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.article-content :where(.callout h1) {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
const CALLOUT_LABELS = new Map([
|
||||
["aside", "Aside"],
|
||||
["caution", "Caution"],
|
||||
["note", "Note"],
|
||||
["tip", "Tip"],
|
||||
["warning", "Warning"],
|
||||
const CALLOUT_TYPES = new Set([
|
||||
"aside",
|
||||
"caution",
|
||||
"note",
|
||||
"tip",
|
||||
"warning",
|
||||
]);
|
||||
|
||||
const CALLOUT_MARKERS = new Map([
|
||||
["📝", "Note"],
|
||||
["💡", "Tip"],
|
||||
["⚡", "Tip"],
|
||||
["⚠️", "Warning"],
|
||||
["⚠", "Warning"],
|
||||
["📝", "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;
|
||||
|
|
@ -21,7 +23,7 @@ function textFromInline(node) {
|
|||
return node.children.map(textFromInline).join("");
|
||||
}
|
||||
|
||||
function normalizeLabel(value) {
|
||||
function normalizeType(value) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^["“”]+|["“”]+$/g, "")
|
||||
|
|
@ -29,48 +31,99 @@ function normalizeLabel(value) {
|
|||
.toLowerCase();
|
||||
}
|
||||
|
||||
function paragraphCalloutLabel(paragraph) {
|
||||
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 match = first.value.match(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i);
|
||||
if (match) {
|
||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
||||
const bracket = parseBracketCallout(first.value);
|
||||
if (bracket?.type) {
|
||||
return bracket;
|
||||
}
|
||||
|
||||
const trimmed = first.value.trimStart();
|
||||
for (const [marker, label] of CALLOUT_MARKERS) {
|
||||
for (const [marker, type] of CALLOUT_MARKERS) {
|
||||
if (trimmed.startsWith(marker)) {
|
||||
return label;
|
||||
return { kind: "marker", type, title: "", marker };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first?.type !== "strong") return null;
|
||||
|
||||
const label = CALLOUT_LABELS.get(normalizeLabel(textFromInline(first)));
|
||||
if (!label) return null;
|
||||
const type = calloutType(textFromInline(first));
|
||||
if (!type) return null;
|
||||
|
||||
return label;
|
||||
return { kind: "strong", type, title: "" };
|
||||
}
|
||||
|
||||
function removeLeadingLabel(paragraph, label) {
|
||||
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 (first?.type === "text") {
|
||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
||||
for (const marker of CALLOUT_MARKERS.keys()) {
|
||||
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
first.value = first.value.replace(new RegExp(`^\\s*${escapedMarker}\\s*`, "u"), "");
|
||||
}
|
||||
if (!first.value) paragraph.children.shift();
|
||||
if (callout.kind === "bracket" && first?.type === "text") {
|
||||
first.value = first.value.replace(BRACKET_CALLOUT_PATTERN, "");
|
||||
cleanupParagraphStart(paragraph);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstText = normalizeLabel(textFromInline(first));
|
||||
if (!first || first.type !== "strong" || firstText !== label.toLowerCase()) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -81,25 +134,30 @@ function removeLeadingLabel(paragraph, label) {
|
|||
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 label = paragraphCalloutLabel(first);
|
||||
if (!label) return;
|
||||
const callout = paragraphCallout(first);
|
||||
if (!callout) return;
|
||||
|
||||
removeLeadingLabel(first, label);
|
||||
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-${label.toLowerCase()}`],
|
||||
dataCalloutLabel: label,
|
||||
className: ["callout", `callout-${callout.type}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,6 +221,15 @@
|
|||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.article-content :where(.callout-title) {
|
||||
margin: 0 0 0.45rem;
|
||||
color: color-mix(in srgb, hsl(var(--primary)) 78%, hsl(var(--foreground)));
|
||||
font-family: var(--font-fraunces);
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.article-content :where(.callout h1) {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
const CALLOUT_LABELS = new Map([
|
||||
["aside", "Aside"],
|
||||
["caution", "Caution"],
|
||||
["note", "Note"],
|
||||
["tip", "Tip"],
|
||||
["warning", "Warning"],
|
||||
const CALLOUT_TYPES = new Set([
|
||||
"aside",
|
||||
"caution",
|
||||
"note",
|
||||
"tip",
|
||||
"warning",
|
||||
]);
|
||||
|
||||
const CALLOUT_MARKERS = new Map([
|
||||
["📝", "Note"],
|
||||
["💡", "Tip"],
|
||||
["⚡", "Tip"],
|
||||
["⚠️", "Warning"],
|
||||
["⚠", "Warning"],
|
||||
["📝", "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;
|
||||
|
|
@ -21,7 +23,7 @@ function textFromInline(node) {
|
|||
return node.children.map(textFromInline).join("");
|
||||
}
|
||||
|
||||
function normalizeLabel(value) {
|
||||
function normalizeType(value) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^["“”]+|["“”]+$/g, "")
|
||||
|
|
@ -29,48 +31,99 @@ function normalizeLabel(value) {
|
|||
.toLowerCase();
|
||||
}
|
||||
|
||||
function paragraphCalloutLabel(paragraph) {
|
||||
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 match = first.value.match(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i);
|
||||
if (match) {
|
||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
||||
const bracket = parseBracketCallout(first.value);
|
||||
if (bracket?.type) {
|
||||
return bracket;
|
||||
}
|
||||
|
||||
const trimmed = first.value.trimStart();
|
||||
for (const [marker, label] of CALLOUT_MARKERS) {
|
||||
for (const [marker, type] of CALLOUT_MARKERS) {
|
||||
if (trimmed.startsWith(marker)) {
|
||||
return label;
|
||||
return { kind: "marker", type, title: "", marker };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first?.type !== "strong") return null;
|
||||
|
||||
const label = CALLOUT_LABELS.get(normalizeLabel(textFromInline(first)));
|
||||
if (!label) return null;
|
||||
const type = calloutType(textFromInline(first));
|
||||
if (!type) return null;
|
||||
|
||||
return label;
|
||||
return { kind: "strong", type, title: "" };
|
||||
}
|
||||
|
||||
function removeLeadingLabel(paragraph, label) {
|
||||
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 (first?.type === "text") {
|
||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
||||
for (const marker of CALLOUT_MARKERS.keys()) {
|
||||
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
first.value = first.value.replace(new RegExp(`^\\s*${escapedMarker}\\s*`, "u"), "");
|
||||
}
|
||||
if (!first.value) paragraph.children.shift();
|
||||
if (callout.kind === "bracket" && first?.type === "text") {
|
||||
first.value = first.value.replace(BRACKET_CALLOUT_PATTERN, "");
|
||||
cleanupParagraphStart(paragraph);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstText = normalizeLabel(textFromInline(first));
|
||||
if (!first || first.type !== "strong" || firstText !== label.toLowerCase()) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -81,25 +134,30 @@ function removeLeadingLabel(paragraph, label) {
|
|||
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 label = paragraphCalloutLabel(first);
|
||||
if (!label) return;
|
||||
const callout = paragraphCallout(first);
|
||||
if (!callout) return;
|
||||
|
||||
removeLeadingLabel(first, label);
|
||||
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-${label.toLowerCase()}`],
|
||||
dataCalloutLabel: label,
|
||||
className: ["callout", `callout-${callout.type}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,6 +221,15 @@
|
|||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.article-content :where(.callout-title) {
|
||||
margin: 0 0 0.45rem;
|
||||
color: color-mix(in srgb, hsl(var(--primary)) 78%, hsl(var(--foreground)));
|
||||
font-family: var(--font-fraunces);
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.article-content :where(.callout h1) {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
const CALLOUT_LABELS = new Map([
|
||||
["aside", "Aside"],
|
||||
["caution", "Caution"],
|
||||
["note", "Note"],
|
||||
["tip", "Tip"],
|
||||
["warning", "Warning"],
|
||||
const CALLOUT_TYPES = new Set([
|
||||
"aside",
|
||||
"caution",
|
||||
"note",
|
||||
"tip",
|
||||
"warning",
|
||||
]);
|
||||
|
||||
const CALLOUT_MARKERS = new Map([
|
||||
["📝", "Note"],
|
||||
["💡", "Tip"],
|
||||
["⚡", "Tip"],
|
||||
["⚠️", "Warning"],
|
||||
["⚠", "Warning"],
|
||||
["📝", "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;
|
||||
|
|
@ -21,7 +23,7 @@ function textFromInline(node) {
|
|||
return node.children.map(textFromInline).join("");
|
||||
}
|
||||
|
||||
function normalizeLabel(value) {
|
||||
function normalizeType(value) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^["“”]+|["“”]+$/g, "")
|
||||
|
|
@ -29,48 +31,99 @@ function normalizeLabel(value) {
|
|||
.toLowerCase();
|
||||
}
|
||||
|
||||
function paragraphCalloutLabel(paragraph) {
|
||||
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 match = first.value.match(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i);
|
||||
if (match) {
|
||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
||||
const bracket = parseBracketCallout(first.value);
|
||||
if (bracket?.type) {
|
||||
return bracket;
|
||||
}
|
||||
|
||||
const trimmed = first.value.trimStart();
|
||||
for (const [marker, label] of CALLOUT_MARKERS) {
|
||||
for (const [marker, type] of CALLOUT_MARKERS) {
|
||||
if (trimmed.startsWith(marker)) {
|
||||
return label;
|
||||
return { kind: "marker", type, title: "", marker };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first?.type !== "strong") return null;
|
||||
|
||||
const label = CALLOUT_LABELS.get(normalizeLabel(textFromInline(first)));
|
||||
if (!label) return null;
|
||||
const type = calloutType(textFromInline(first));
|
||||
if (!type) return null;
|
||||
|
||||
return label;
|
||||
return { kind: "strong", type, title: "" };
|
||||
}
|
||||
|
||||
function removeLeadingLabel(paragraph, label) {
|
||||
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 (first?.type === "text") {
|
||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
||||
for (const marker of CALLOUT_MARKERS.keys()) {
|
||||
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
first.value = first.value.replace(new RegExp(`^\\s*${escapedMarker}\\s*`, "u"), "");
|
||||
}
|
||||
if (!first.value) paragraph.children.shift();
|
||||
if (callout.kind === "bracket" && first?.type === "text") {
|
||||
first.value = first.value.replace(BRACKET_CALLOUT_PATTERN, "");
|
||||
cleanupParagraphStart(paragraph);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstText = normalizeLabel(textFromInline(first));
|
||||
if (!first || first.type !== "strong" || firstText !== label.toLowerCase()) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -81,25 +134,30 @@ function removeLeadingLabel(paragraph, label) {
|
|||
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 label = paragraphCalloutLabel(first);
|
||||
if (!label) return;
|
||||
const callout = paragraphCallout(first);
|
||||
if (!callout) return;
|
||||
|
||||
removeLeadingLabel(first, label);
|
||||
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-${label.toLowerCase()}`],
|
||||
dataCalloutLabel: label,
|
||||
className: ["callout", `callout-${callout.type}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,6 +221,15 @@
|
|||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.article-content :where(.callout-title) {
|
||||
margin: 0 0 0.45rem;
|
||||
color: color-mix(in srgb, hsl(var(--primary)) 78%, hsl(var(--foreground)));
|
||||
font-family: var(--font-fraunces);
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.article-content :where(.callout h1) {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
const CALLOUT_LABELS = new Map([
|
||||
["aside", "Aside"],
|
||||
["caution", "Caution"],
|
||||
["note", "Note"],
|
||||
["tip", "Tip"],
|
||||
["warning", "Warning"],
|
||||
const CALLOUT_TYPES = new Set([
|
||||
"aside",
|
||||
"caution",
|
||||
"note",
|
||||
"tip",
|
||||
"warning",
|
||||
]);
|
||||
|
||||
const CALLOUT_MARKERS = new Map([
|
||||
["📝", "Note"],
|
||||
["💡", "Tip"],
|
||||
["⚡", "Tip"],
|
||||
["⚠️", "Warning"],
|
||||
["⚠", "Warning"],
|
||||
["📝", "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;
|
||||
|
|
@ -21,7 +23,7 @@ function textFromInline(node) {
|
|||
return node.children.map(textFromInline).join("");
|
||||
}
|
||||
|
||||
function normalizeLabel(value) {
|
||||
function normalizeType(value) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^["“”]+|["“”]+$/g, "")
|
||||
|
|
@ -29,48 +31,99 @@ function normalizeLabel(value) {
|
|||
.toLowerCase();
|
||||
}
|
||||
|
||||
function paragraphCalloutLabel(paragraph) {
|
||||
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 match = first.value.match(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i);
|
||||
if (match) {
|
||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
||||
const bracket = parseBracketCallout(first.value);
|
||||
if (bracket?.type) {
|
||||
return bracket;
|
||||
}
|
||||
|
||||
const trimmed = first.value.trimStart();
|
||||
for (const [marker, label] of CALLOUT_MARKERS) {
|
||||
for (const [marker, type] of CALLOUT_MARKERS) {
|
||||
if (trimmed.startsWith(marker)) {
|
||||
return label;
|
||||
return { kind: "marker", type, title: "", marker };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (first?.type !== "strong") return null;
|
||||
|
||||
const label = CALLOUT_LABELS.get(normalizeLabel(textFromInline(first)));
|
||||
if (!label) return null;
|
||||
const type = calloutType(textFromInline(first));
|
||||
if (!type) return null;
|
||||
|
||||
return label;
|
||||
return { kind: "strong", type, title: "" };
|
||||
}
|
||||
|
||||
function removeLeadingLabel(paragraph, label) {
|
||||
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 (first?.type === "text") {
|
||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
||||
for (const marker of CALLOUT_MARKERS.keys()) {
|
||||
const escapedMarker = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
first.value = first.value.replace(new RegExp(`^\\s*${escapedMarker}\\s*`, "u"), "");
|
||||
}
|
||||
if (!first.value) paragraph.children.shift();
|
||||
if (callout.kind === "bracket" && first?.type === "text") {
|
||||
first.value = first.value.replace(BRACKET_CALLOUT_PATTERN, "");
|
||||
cleanupParagraphStart(paragraph);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstText = normalizeLabel(textFromInline(first));
|
||||
if (!first || first.type !== "strong" || firstText !== label.toLowerCase()) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -81,25 +134,30 @@ function removeLeadingLabel(paragraph, label) {
|
|||
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 label = paragraphCalloutLabel(first);
|
||||
if (!label) return;
|
||||
const callout = paragraphCallout(first);
|
||||
if (!callout) return;
|
||||
|
||||
removeLeadingLabel(first, label);
|
||||
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-${label.toLowerCase()}`],
|
||||
dataCalloutLabel: label,
|
||||
className: ["callout", `callout-${callout.type}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,6 +221,15 @@
|
|||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.article-content :where(.callout-title) {
|
||||
margin: 0 0 0.45rem;
|
||||
color: color-mix(in srgb, hsl(var(--primary)) 78%, hsl(var(--foreground)));
|
||||
font-family: var(--font-fraunces);
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.article-content :where(.callout h1) {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,21 @@
|
|||
const CALLOUT_LABELS = new Map([
|
||||
["aside", "Aside"],
|
||||
["caution", "Caution"],
|
||||
["note", "Note"],
|
||||
["tip", "Tip"],
|
||||
["warning", "Warning"],
|
||||
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;
|
||||
|
|
@ -13,7 +23,7 @@ function textFromInline(node) {
|
|||
return node.children.map(textFromInline).join("");
|
||||
}
|
||||
|
||||
function normalizeLabel(value) {
|
||||
function normalizeType(value) {
|
||||
return value
|
||||
.trim()
|
||||
.replace(/^["“”]+|["“”]+$/g, "")
|
||||
|
|
@ -21,37 +31,99 @@ function normalizeLabel(value) {
|
|||
.toLowerCase();
|
||||
}
|
||||
|
||||
function paragraphCalloutLabel(paragraph) {
|
||||
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 match = first.value.match(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i);
|
||||
if (match) {
|
||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
||||
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 label = CALLOUT_LABELS.get(normalizeLabel(textFromInline(first)));
|
||||
if (!label) return null;
|
||||
const type = calloutType(textFromInline(first));
|
||||
if (!type) return null;
|
||||
|
||||
return label;
|
||||
return { kind: "strong", type, title: "" };
|
||||
}
|
||||
|
||||
function removeLeadingLabel(paragraph, label) {
|
||||
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 (first?.type === "text") {
|
||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
||||
if (!first.value) paragraph.children.shift();
|
||||
if (callout.kind === "bracket" && first?.type === "text") {
|
||||
first.value = first.value.replace(BRACKET_CALLOUT_PATTERN, "");
|
||||
cleanupParagraphStart(paragraph);
|
||||
return;
|
||||
}
|
||||
|
||||
const firstText = normalizeLabel(textFromInline(first));
|
||||
if (!first || first.type !== "strong" || firstText !== label.toLowerCase()) {
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -62,25 +134,30 @@ function removeLeadingLabel(paragraph, label) {
|
|||
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 label = paragraphCalloutLabel(first);
|
||||
if (!label) return;
|
||||
const callout = paragraphCallout(first);
|
||||
if (!callout) return;
|
||||
|
||||
removeLeadingLabel(first, label);
|
||||
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-${label.toLowerCase()}`],
|
||||
dataCalloutLabel: label,
|
||||
className: ["callout", `callout-${callout.type}`],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,6 +200,15 @@
|
|||
margin-top: 0.8rem;
|
||||
}
|
||||
|
||||
.article-content :where(.callout-title) {
|
||||
margin: 0 0 0.45rem;
|
||||
color: color-mix(in srgb, hsl(var(--primary)) 78%, hsl(var(--foreground)));
|
||||
font-family: var(--font-fraunces);
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.article-content :where(.callout h1) {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue