Improve callouts and symbol publishing
Some checks are pending
Build / build (push) Waiting to run

This commit is contained in:
Neeldhara Misra 2026-06-25 15:48:33 +05:30
parent 6439559017
commit 7aa1fd2372
31 changed files with 1939 additions and 12 deletions

View file

@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
import remarkSymbols from "./src/remark/symbols.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
@ -24,6 +25,7 @@ const remarkPlugins = [
remarkMath,
remarkTypography,
remarkCallouts,
remarkSymbols,
];
const rehypePlugins = [rehypeKatex];

View file

@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
["warning", "Warning"],
]);
const CALLOUT_MARKERS = new Map([
["📝", "Note"],
["💡", "Tip"],
["⚡", "Tip"],
["⚠️", "Warning"],
["⚠", "Warning"],
]);
function textFromInline(node) {
if (!node) return "";
if (typeof node.value === "string") return node.value;
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
if (match) {
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
}
const trimmed = first.value.trimStart();
for (const [marker, label] of CALLOUT_MARKERS) {
if (trimmed.startsWith(marker)) {
return label;
}
}
}
if (first?.type !== "strong") return null;
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
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();
return;
}

View file

@ -0,0 +1,160 @@
const CHESS_LABELS = {
"♔": "white king",
"♕": "white queen",
"♖": "white rook",
"♗": "white bishop",
"♘": "white knight",
"♙": "white pawn",
"♚": "black king",
"♛": "black queen",
"♜": "black rook",
"♝": "black bishop",
"♞": "black knight",
"♟": "black pawn",
};
const CARD_LABELS = {
"♠": "spade",
"♤": "spade",
"♣": "club",
"♧": "club",
"♥": "heart",
"♡": "heart",
"♦": "diamond",
"♢": "diamond",
};
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function isVariationSelector(codePoint) {
return codePoint === 0xfe0e || codePoint === 0xfe0f;
}
function isSkinTone(codePoint) {
return codePoint >= 0x1f3fb && codePoint <= 0x1f3ff;
}
function isEmojiBase(codePoint) {
return (
(codePoint >= 0x1f000 && codePoint <= 0x1faff) ||
(codePoint >= 0x2600 && codePoint <= 0x27bf) ||
codePoint === 0x00a9 ||
codePoint === 0x00ae ||
codePoint === 0x203c ||
codePoint === 0x2049 ||
codePoint === 0x2122 ||
codePoint === 0x2139 ||
codePoint === 0x3030 ||
codePoint === 0x303d ||
codePoint === 0x3297 ||
codePoint === 0x3299
);
}
function readCluster(chars, index) {
let value = chars[index];
let next = index + 1;
while (next < chars.length) {
const codePoint = chars[next].codePointAt(0);
if (isVariationSelector(codePoint) || isSkinTone(codePoint)) {
value += chars[next];
next += 1;
continue;
}
if (codePoint === 0x200d && next + 1 < chars.length) {
value += chars[next] + chars[next + 1];
next += 2;
continue;
}
break;
}
return { value, next };
}
function symbolHtml(cluster) {
if (CHESS_LABELS[cluster]) {
return `<span class="symbol symbol-chess" data-symbol="${escapeHtml(CHESS_LABELS[cluster].replaceAll(" ", "-"))}" aria-label="${escapeHtml(CHESS_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
if (CARD_LABELS[cluster]) {
return `<span class="symbol symbol-card" data-symbol="${escapeHtml(CARD_LABELS[cluster])}" aria-label="${escapeHtml(CARD_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
const codePoint = cluster.codePointAt(0);
if ((codePoint >= 0x1f0a0 && codePoint <= 0x1f0ff) || isEmojiBase(codePoint)) {
return `<span class="symbol symbol-emoji">${escapeHtml(cluster)}</span>`;
}
return null;
}
function splitText(value) {
const chars = Array.from(value);
const nodes = [];
let text = "";
function flushText() {
if (text) {
nodes.push({ type: "text", value: text });
text = "";
}
}
for (let index = 0; index < chars.length; ) {
const codePoint = chars[index].codePointAt(0);
const shouldCluster =
CHESS_LABELS[chars[index]] ||
CARD_LABELS[chars[index]] ||
isEmojiBase(codePoint);
if (!shouldCluster) {
text += chars[index];
index += 1;
continue;
}
const { value: cluster, next } = readCluster(chars, index);
const html = symbolHtml(cluster);
if (html) {
flushText();
nodes.push({ type: "html", value: html });
} else {
text += cluster;
}
index = next;
}
flushText();
return nodes;
}
function transformChildren(parent) {
if (!Array.isArray(parent?.children)) return;
const nextChildren = [];
for (const child of parent.children) {
if (child.type === "text") {
nextChildren.push(...splitText(child.value));
continue;
}
transformChildren(child);
nextChildren.push(child);
}
parent.children = nextChildren;
}
export default function remarkSymbols() {
return (tree) => transformChildren(tree);
}

View file

@ -66,7 +66,9 @@
.article-content {
color: hsl(var(--foreground));
font-family: var(--font-heliotrope);
font-family:
var(--font-heliotrope), "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
"Noto Sans Symbols 2", "DejaVu Sans", sans-serif;
font-size: 1.0625rem;
line-height: 1.78;
}
@ -129,6 +131,25 @@
text-underline-offset: 0.18em;
}
.article-content :where(.symbol) {
display: inline-block;
line-height: 1;
vertical-align: -0.08em;
}
.article-content :where(.symbol-emoji) {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", emoji, sans-serif;
}
.article-content :where(.symbol-chess, .symbol-card) {
font-family: "Noto Sans Symbols 2", "DejaVu Sans", "Symbola", serif;
font-size: 1.04em;
}
.article-content :where(.symbol-card[data-symbol="heart"], .symbol-card[data-symbol="diamond"]) {
color: color-mix(in srgb, crimson 80%, hsl(var(--foreground)));
}
.article-content :where(code) {
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;

View file

@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
import remarkSymbols from "./src/remark/symbols.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
@ -24,6 +25,7 @@ const remarkPlugins = [
remarkMath,
remarkTypography,
remarkCallouts,
remarkSymbols,
];
const rehypePlugins = [rehypeKatex];

View file

@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
["warning", "Warning"],
]);
const CALLOUT_MARKERS = new Map([
["📝", "Note"],
["💡", "Tip"],
["⚡", "Tip"],
["⚠️", "Warning"],
["⚠", "Warning"],
]);
function textFromInline(node) {
if (!node) return "";
if (typeof node.value === "string") return node.value;
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
if (match) {
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
}
const trimmed = first.value.trimStart();
for (const [marker, label] of CALLOUT_MARKERS) {
if (trimmed.startsWith(marker)) {
return label;
}
}
}
if (first?.type !== "strong") return null;
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
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();
return;
}

View file

@ -0,0 +1,160 @@
const CHESS_LABELS = {
"♔": "white king",
"♕": "white queen",
"♖": "white rook",
"♗": "white bishop",
"♘": "white knight",
"♙": "white pawn",
"♚": "black king",
"♛": "black queen",
"♜": "black rook",
"♝": "black bishop",
"♞": "black knight",
"♟": "black pawn",
};
const CARD_LABELS = {
"♠": "spade",
"♤": "spade",
"♣": "club",
"♧": "club",
"♥": "heart",
"♡": "heart",
"♦": "diamond",
"♢": "diamond",
};
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function isVariationSelector(codePoint) {
return codePoint === 0xfe0e || codePoint === 0xfe0f;
}
function isSkinTone(codePoint) {
return codePoint >= 0x1f3fb && codePoint <= 0x1f3ff;
}
function isEmojiBase(codePoint) {
return (
(codePoint >= 0x1f000 && codePoint <= 0x1faff) ||
(codePoint >= 0x2600 && codePoint <= 0x27bf) ||
codePoint === 0x00a9 ||
codePoint === 0x00ae ||
codePoint === 0x203c ||
codePoint === 0x2049 ||
codePoint === 0x2122 ||
codePoint === 0x2139 ||
codePoint === 0x3030 ||
codePoint === 0x303d ||
codePoint === 0x3297 ||
codePoint === 0x3299
);
}
function readCluster(chars, index) {
let value = chars[index];
let next = index + 1;
while (next < chars.length) {
const codePoint = chars[next].codePointAt(0);
if (isVariationSelector(codePoint) || isSkinTone(codePoint)) {
value += chars[next];
next += 1;
continue;
}
if (codePoint === 0x200d && next + 1 < chars.length) {
value += chars[next] + chars[next + 1];
next += 2;
continue;
}
break;
}
return { value, next };
}
function symbolHtml(cluster) {
if (CHESS_LABELS[cluster]) {
return `<span class="symbol symbol-chess" data-symbol="${escapeHtml(CHESS_LABELS[cluster].replaceAll(" ", "-"))}" aria-label="${escapeHtml(CHESS_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
if (CARD_LABELS[cluster]) {
return `<span class="symbol symbol-card" data-symbol="${escapeHtml(CARD_LABELS[cluster])}" aria-label="${escapeHtml(CARD_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
const codePoint = cluster.codePointAt(0);
if ((codePoint >= 0x1f0a0 && codePoint <= 0x1f0ff) || isEmojiBase(codePoint)) {
return `<span class="symbol symbol-emoji">${escapeHtml(cluster)}</span>`;
}
return null;
}
function splitText(value) {
const chars = Array.from(value);
const nodes = [];
let text = "";
function flushText() {
if (text) {
nodes.push({ type: "text", value: text });
text = "";
}
}
for (let index = 0; index < chars.length; ) {
const codePoint = chars[index].codePointAt(0);
const shouldCluster =
CHESS_LABELS[chars[index]] ||
CARD_LABELS[chars[index]] ||
isEmojiBase(codePoint);
if (!shouldCluster) {
text += chars[index];
index += 1;
continue;
}
const { value: cluster, next } = readCluster(chars, index);
const html = symbolHtml(cluster);
if (html) {
flushText();
nodes.push({ type: "html", value: html });
} else {
text += cluster;
}
index = next;
}
flushText();
return nodes;
}
function transformChildren(parent) {
if (!Array.isArray(parent?.children)) return;
const nextChildren = [];
for (const child of parent.children) {
if (child.type === "text") {
nextChildren.push(...splitText(child.value));
continue;
}
transformChildren(child);
nextChildren.push(child);
}
parent.children = nextChildren;
}
export default function remarkSymbols() {
return (tree) => transformChildren(tree);
}

View file

@ -66,7 +66,9 @@
.article-content {
color: hsl(var(--foreground));
font-family: var(--font-heliotrope);
font-family:
var(--font-heliotrope), "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
"Noto Sans Symbols 2", "DejaVu Sans", sans-serif;
font-size: 1.0625rem;
line-height: 1.78;
}
@ -129,6 +131,25 @@
text-underline-offset: 0.18em;
}
.article-content :where(.symbol) {
display: inline-block;
line-height: 1;
vertical-align: -0.08em;
}
.article-content :where(.symbol-emoji) {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", emoji, sans-serif;
}
.article-content :where(.symbol-chess, .symbol-card) {
font-family: "Noto Sans Symbols 2", "DejaVu Sans", "Symbola", serif;
font-size: 1.04em;
}
.article-content :where(.symbol-card[data-symbol="heart"], .symbol-card[data-symbol="diamond"]) {
color: color-mix(in srgb, crimson 80%, hsl(var(--foreground)));
}
.article-content :where(code) {
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;

View file

@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
import remarkSymbols from "./src/remark/symbols.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
@ -24,6 +25,7 @@ const remarkPlugins = [
remarkMath,
remarkTypography,
remarkCallouts,
remarkSymbols,
];
const rehypePlugins = [rehypeKatex];

View file

@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
["warning", "Warning"],
]);
const CALLOUT_MARKERS = new Map([
["📝", "Note"],
["💡", "Tip"],
["⚡", "Tip"],
["⚠️", "Warning"],
["⚠", "Warning"],
]);
function textFromInline(node) {
if (!node) return "";
if (typeof node.value === "string") return node.value;
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
if (match) {
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
}
const trimmed = first.value.trimStart();
for (const [marker, label] of CALLOUT_MARKERS) {
if (trimmed.startsWith(marker)) {
return label;
}
}
}
if (first?.type !== "strong") return null;
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
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();
return;
}

View file

@ -0,0 +1,160 @@
const CHESS_LABELS = {
"♔": "white king",
"♕": "white queen",
"♖": "white rook",
"♗": "white bishop",
"♘": "white knight",
"♙": "white pawn",
"♚": "black king",
"♛": "black queen",
"♜": "black rook",
"♝": "black bishop",
"♞": "black knight",
"♟": "black pawn",
};
const CARD_LABELS = {
"♠": "spade",
"♤": "spade",
"♣": "club",
"♧": "club",
"♥": "heart",
"♡": "heart",
"♦": "diamond",
"♢": "diamond",
};
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function isVariationSelector(codePoint) {
return codePoint === 0xfe0e || codePoint === 0xfe0f;
}
function isSkinTone(codePoint) {
return codePoint >= 0x1f3fb && codePoint <= 0x1f3ff;
}
function isEmojiBase(codePoint) {
return (
(codePoint >= 0x1f000 && codePoint <= 0x1faff) ||
(codePoint >= 0x2600 && codePoint <= 0x27bf) ||
codePoint === 0x00a9 ||
codePoint === 0x00ae ||
codePoint === 0x203c ||
codePoint === 0x2049 ||
codePoint === 0x2122 ||
codePoint === 0x2139 ||
codePoint === 0x3030 ||
codePoint === 0x303d ||
codePoint === 0x3297 ||
codePoint === 0x3299
);
}
function readCluster(chars, index) {
let value = chars[index];
let next = index + 1;
while (next < chars.length) {
const codePoint = chars[next].codePointAt(0);
if (isVariationSelector(codePoint) || isSkinTone(codePoint)) {
value += chars[next];
next += 1;
continue;
}
if (codePoint === 0x200d && next + 1 < chars.length) {
value += chars[next] + chars[next + 1];
next += 2;
continue;
}
break;
}
return { value, next };
}
function symbolHtml(cluster) {
if (CHESS_LABELS[cluster]) {
return `<span class="symbol symbol-chess" data-symbol="${escapeHtml(CHESS_LABELS[cluster].replaceAll(" ", "-"))}" aria-label="${escapeHtml(CHESS_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
if (CARD_LABELS[cluster]) {
return `<span class="symbol symbol-card" data-symbol="${escapeHtml(CARD_LABELS[cluster])}" aria-label="${escapeHtml(CARD_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
const codePoint = cluster.codePointAt(0);
if ((codePoint >= 0x1f0a0 && codePoint <= 0x1f0ff) || isEmojiBase(codePoint)) {
return `<span class="symbol symbol-emoji">${escapeHtml(cluster)}</span>`;
}
return null;
}
function splitText(value) {
const chars = Array.from(value);
const nodes = [];
let text = "";
function flushText() {
if (text) {
nodes.push({ type: "text", value: text });
text = "";
}
}
for (let index = 0; index < chars.length; ) {
const codePoint = chars[index].codePointAt(0);
const shouldCluster =
CHESS_LABELS[chars[index]] ||
CARD_LABELS[chars[index]] ||
isEmojiBase(codePoint);
if (!shouldCluster) {
text += chars[index];
index += 1;
continue;
}
const { value: cluster, next } = readCluster(chars, index);
const html = symbolHtml(cluster);
if (html) {
flushText();
nodes.push({ type: "html", value: html });
} else {
text += cluster;
}
index = next;
}
flushText();
return nodes;
}
function transformChildren(parent) {
if (!Array.isArray(parent?.children)) return;
const nextChildren = [];
for (const child of parent.children) {
if (child.type === "text") {
nextChildren.push(...splitText(child.value));
continue;
}
transformChildren(child);
nextChildren.push(child);
}
parent.children = nextChildren;
}
export default function remarkSymbols() {
return (tree) => transformChildren(tree);
}

View file

@ -66,7 +66,9 @@
.article-content {
color: hsl(var(--foreground));
font-family: var(--font-heliotrope);
font-family:
var(--font-heliotrope), "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
"Noto Sans Symbols 2", "DejaVu Sans", sans-serif;
font-size: 1.0625rem;
line-height: 1.78;
}
@ -129,6 +131,25 @@
text-underline-offset: 0.18em;
}
.article-content :where(.symbol) {
display: inline-block;
line-height: 1;
vertical-align: -0.08em;
}
.article-content :where(.symbol-emoji) {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", emoji, sans-serif;
}
.article-content :where(.symbol-chess, .symbol-card) {
font-family: "Noto Sans Symbols 2", "DejaVu Sans", "Symbola", serif;
font-size: 1.04em;
}
.article-content :where(.symbol-card[data-symbol="heart"], .symbol-card[data-symbol="diamond"]) {
color: color-mix(in srgb, crimson 80%, hsl(var(--foreground)));
}
.article-content :where(code) {
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;

View file

@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
import remarkSymbols from "./src/remark/symbols.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
@ -24,6 +25,7 @@ const remarkPlugins = [
remarkMath,
remarkTypography,
remarkCallouts,
remarkSymbols,
];
const rehypePlugins = [rehypeKatex];

View file

@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
["warning", "Warning"],
]);
const CALLOUT_MARKERS = new Map([
["📝", "Note"],
["💡", "Tip"],
["⚡", "Tip"],
["⚠️", "Warning"],
["⚠", "Warning"],
]);
function textFromInline(node) {
if (!node) return "";
if (typeof node.value === "string") return node.value;
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
if (match) {
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
}
const trimmed = first.value.trimStart();
for (const [marker, label] of CALLOUT_MARKERS) {
if (trimmed.startsWith(marker)) {
return label;
}
}
}
if (first?.type !== "strong") return null;
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
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();
return;
}

View file

@ -0,0 +1,160 @@
const CHESS_LABELS = {
"♔": "white king",
"♕": "white queen",
"♖": "white rook",
"♗": "white bishop",
"♘": "white knight",
"♙": "white pawn",
"♚": "black king",
"♛": "black queen",
"♜": "black rook",
"♝": "black bishop",
"♞": "black knight",
"♟": "black pawn",
};
const CARD_LABELS = {
"♠": "spade",
"♤": "spade",
"♣": "club",
"♧": "club",
"♥": "heart",
"♡": "heart",
"♦": "diamond",
"♢": "diamond",
};
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function isVariationSelector(codePoint) {
return codePoint === 0xfe0e || codePoint === 0xfe0f;
}
function isSkinTone(codePoint) {
return codePoint >= 0x1f3fb && codePoint <= 0x1f3ff;
}
function isEmojiBase(codePoint) {
return (
(codePoint >= 0x1f000 && codePoint <= 0x1faff) ||
(codePoint >= 0x2600 && codePoint <= 0x27bf) ||
codePoint === 0x00a9 ||
codePoint === 0x00ae ||
codePoint === 0x203c ||
codePoint === 0x2049 ||
codePoint === 0x2122 ||
codePoint === 0x2139 ||
codePoint === 0x3030 ||
codePoint === 0x303d ||
codePoint === 0x3297 ||
codePoint === 0x3299
);
}
function readCluster(chars, index) {
let value = chars[index];
let next = index + 1;
while (next < chars.length) {
const codePoint = chars[next].codePointAt(0);
if (isVariationSelector(codePoint) || isSkinTone(codePoint)) {
value += chars[next];
next += 1;
continue;
}
if (codePoint === 0x200d && next + 1 < chars.length) {
value += chars[next] + chars[next + 1];
next += 2;
continue;
}
break;
}
return { value, next };
}
function symbolHtml(cluster) {
if (CHESS_LABELS[cluster]) {
return `<span class="symbol symbol-chess" data-symbol="${escapeHtml(CHESS_LABELS[cluster].replaceAll(" ", "-"))}" aria-label="${escapeHtml(CHESS_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
if (CARD_LABELS[cluster]) {
return `<span class="symbol symbol-card" data-symbol="${escapeHtml(CARD_LABELS[cluster])}" aria-label="${escapeHtml(CARD_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
const codePoint = cluster.codePointAt(0);
if ((codePoint >= 0x1f0a0 && codePoint <= 0x1f0ff) || isEmojiBase(codePoint)) {
return `<span class="symbol symbol-emoji">${escapeHtml(cluster)}</span>`;
}
return null;
}
function splitText(value) {
const chars = Array.from(value);
const nodes = [];
let text = "";
function flushText() {
if (text) {
nodes.push({ type: "text", value: text });
text = "";
}
}
for (let index = 0; index < chars.length; ) {
const codePoint = chars[index].codePointAt(0);
const shouldCluster =
CHESS_LABELS[chars[index]] ||
CARD_LABELS[chars[index]] ||
isEmojiBase(codePoint);
if (!shouldCluster) {
text += chars[index];
index += 1;
continue;
}
const { value: cluster, next } = readCluster(chars, index);
const html = symbolHtml(cluster);
if (html) {
flushText();
nodes.push({ type: "html", value: html });
} else {
text += cluster;
}
index = next;
}
flushText();
return nodes;
}
function transformChildren(parent) {
if (!Array.isArray(parent?.children)) return;
const nextChildren = [];
for (const child of parent.children) {
if (child.type === "text") {
nextChildren.push(...splitText(child.value));
continue;
}
transformChildren(child);
nextChildren.push(child);
}
parent.children = nextChildren;
}
export default function remarkSymbols() {
return (tree) => transformChildren(tree);
}

View file

@ -66,7 +66,9 @@
.article-content {
color: hsl(var(--foreground));
font-family: var(--font-heliotrope);
font-family:
var(--font-heliotrope), "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
"Noto Sans Symbols 2", "DejaVu Sans", sans-serif;
font-size: 1.0625rem;
line-height: 1.78;
}
@ -129,6 +131,25 @@
text-underline-offset: 0.18em;
}
.article-content :where(.symbol) {
display: inline-block;
line-height: 1;
vertical-align: -0.08em;
}
.article-content :where(.symbol-emoji) {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", emoji, sans-serif;
}
.article-content :where(.symbol-chess, .symbol-card) {
font-family: "Noto Sans Symbols 2", "DejaVu Sans", "Symbola", serif;
font-size: 1.04em;
}
.article-content :where(.symbol-card[data-symbol="heart"], .symbol-card[data-symbol="diamond"]) {
color: color-mix(in srgb, crimson 80%, hsl(var(--foreground)));
}
.article-content :where(code) {
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;

View file

@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
import remarkSymbols from "./src/remark/symbols.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
@ -24,6 +25,7 @@ const remarkPlugins = [
remarkMath,
remarkTypography,
remarkCallouts,
remarkSymbols,
];
const rehypePlugins = [rehypeKatex];

View file

@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
["warning", "Warning"],
]);
const CALLOUT_MARKERS = new Map([
["📝", "Note"],
["💡", "Tip"],
["⚡", "Tip"],
["⚠️", "Warning"],
["⚠", "Warning"],
]);
function textFromInline(node) {
if (!node) return "";
if (typeof node.value === "string") return node.value;
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
if (match) {
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
}
const trimmed = first.value.trimStart();
for (const [marker, label] of CALLOUT_MARKERS) {
if (trimmed.startsWith(marker)) {
return label;
}
}
}
if (first?.type !== "strong") return null;
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
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();
return;
}

View file

@ -0,0 +1,160 @@
const CHESS_LABELS = {
"♔": "white king",
"♕": "white queen",
"♖": "white rook",
"♗": "white bishop",
"♘": "white knight",
"♙": "white pawn",
"♚": "black king",
"♛": "black queen",
"♜": "black rook",
"♝": "black bishop",
"♞": "black knight",
"♟": "black pawn",
};
const CARD_LABELS = {
"♠": "spade",
"♤": "spade",
"♣": "club",
"♧": "club",
"♥": "heart",
"♡": "heart",
"♦": "diamond",
"♢": "diamond",
};
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function isVariationSelector(codePoint) {
return codePoint === 0xfe0e || codePoint === 0xfe0f;
}
function isSkinTone(codePoint) {
return codePoint >= 0x1f3fb && codePoint <= 0x1f3ff;
}
function isEmojiBase(codePoint) {
return (
(codePoint >= 0x1f000 && codePoint <= 0x1faff) ||
(codePoint >= 0x2600 && codePoint <= 0x27bf) ||
codePoint === 0x00a9 ||
codePoint === 0x00ae ||
codePoint === 0x203c ||
codePoint === 0x2049 ||
codePoint === 0x2122 ||
codePoint === 0x2139 ||
codePoint === 0x3030 ||
codePoint === 0x303d ||
codePoint === 0x3297 ||
codePoint === 0x3299
);
}
function readCluster(chars, index) {
let value = chars[index];
let next = index + 1;
while (next < chars.length) {
const codePoint = chars[next].codePointAt(0);
if (isVariationSelector(codePoint) || isSkinTone(codePoint)) {
value += chars[next];
next += 1;
continue;
}
if (codePoint === 0x200d && next + 1 < chars.length) {
value += chars[next] + chars[next + 1];
next += 2;
continue;
}
break;
}
return { value, next };
}
function symbolHtml(cluster) {
if (CHESS_LABELS[cluster]) {
return `<span class="symbol symbol-chess" data-symbol="${escapeHtml(CHESS_LABELS[cluster].replaceAll(" ", "-"))}" aria-label="${escapeHtml(CHESS_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
if (CARD_LABELS[cluster]) {
return `<span class="symbol symbol-card" data-symbol="${escapeHtml(CARD_LABELS[cluster])}" aria-label="${escapeHtml(CARD_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
const codePoint = cluster.codePointAt(0);
if ((codePoint >= 0x1f0a0 && codePoint <= 0x1f0ff) || isEmojiBase(codePoint)) {
return `<span class="symbol symbol-emoji">${escapeHtml(cluster)}</span>`;
}
return null;
}
function splitText(value) {
const chars = Array.from(value);
const nodes = [];
let text = "";
function flushText() {
if (text) {
nodes.push({ type: "text", value: text });
text = "";
}
}
for (let index = 0; index < chars.length; ) {
const codePoint = chars[index].codePointAt(0);
const shouldCluster =
CHESS_LABELS[chars[index]] ||
CARD_LABELS[chars[index]] ||
isEmojiBase(codePoint);
if (!shouldCluster) {
text += chars[index];
index += 1;
continue;
}
const { value: cluster, next } = readCluster(chars, index);
const html = symbolHtml(cluster);
if (html) {
flushText();
nodes.push({ type: "html", value: html });
} else {
text += cluster;
}
index = next;
}
flushText();
return nodes;
}
function transformChildren(parent) {
if (!Array.isArray(parent?.children)) return;
const nextChildren = [];
for (const child of parent.children) {
if (child.type === "text") {
nextChildren.push(...splitText(child.value));
continue;
}
transformChildren(child);
nextChildren.push(child);
}
parent.children = nextChildren;
}
export default function remarkSymbols() {
return (tree) => transformChildren(tree);
}

View file

@ -66,7 +66,9 @@
.article-content {
color: hsl(var(--foreground));
font-family: var(--font-heliotrope);
font-family:
var(--font-heliotrope), "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
"Noto Sans Symbols 2", "DejaVu Sans", sans-serif;
font-size: 1.0625rem;
line-height: 1.78;
}
@ -129,6 +131,25 @@
text-underline-offset: 0.18em;
}
.article-content :where(.symbol) {
display: inline-block;
line-height: 1;
vertical-align: -0.08em;
}
.article-content :where(.symbol-emoji) {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", emoji, sans-serif;
}
.article-content :where(.symbol-chess, .symbol-card) {
font-family: "Noto Sans Symbols 2", "DejaVu Sans", "Symbola", serif;
font-size: 1.04em;
}
.article-content :where(.symbol-card[data-symbol="heart"], .symbol-card[data-symbol="diamond"]) {
color: color-mix(in srgb, crimson 80%, hsl(var(--foreground)));
}
.article-content :where(code) {
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;

View file

@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
import remarkSymbols from "./src/remark/symbols.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
@ -24,6 +25,7 @@ const remarkPlugins = [
remarkMath,
remarkTypography,
remarkCallouts,
remarkSymbols,
];
const rehypePlugins = [rehypeKatex];

View file

@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
["warning", "Warning"],
]);
const CALLOUT_MARKERS = new Map([
["📝", "Note"],
["💡", "Tip"],
["⚡", "Tip"],
["⚠️", "Warning"],
["⚠", "Warning"],
]);
function textFromInline(node) {
if (!node) return "";
if (typeof node.value === "string") return node.value;
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
if (match) {
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
}
const trimmed = first.value.trimStart();
for (const [marker, label] of CALLOUT_MARKERS) {
if (trimmed.startsWith(marker)) {
return label;
}
}
}
if (first?.type !== "strong") return null;
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
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();
return;
}

View file

@ -0,0 +1,160 @@
const CHESS_LABELS = {
"♔": "white king",
"♕": "white queen",
"♖": "white rook",
"♗": "white bishop",
"♘": "white knight",
"♙": "white pawn",
"♚": "black king",
"♛": "black queen",
"♜": "black rook",
"♝": "black bishop",
"♞": "black knight",
"♟": "black pawn",
};
const CARD_LABELS = {
"♠": "spade",
"♤": "spade",
"♣": "club",
"♧": "club",
"♥": "heart",
"♡": "heart",
"♦": "diamond",
"♢": "diamond",
};
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function isVariationSelector(codePoint) {
return codePoint === 0xfe0e || codePoint === 0xfe0f;
}
function isSkinTone(codePoint) {
return codePoint >= 0x1f3fb && codePoint <= 0x1f3ff;
}
function isEmojiBase(codePoint) {
return (
(codePoint >= 0x1f000 && codePoint <= 0x1faff) ||
(codePoint >= 0x2600 && codePoint <= 0x27bf) ||
codePoint === 0x00a9 ||
codePoint === 0x00ae ||
codePoint === 0x203c ||
codePoint === 0x2049 ||
codePoint === 0x2122 ||
codePoint === 0x2139 ||
codePoint === 0x3030 ||
codePoint === 0x303d ||
codePoint === 0x3297 ||
codePoint === 0x3299
);
}
function readCluster(chars, index) {
let value = chars[index];
let next = index + 1;
while (next < chars.length) {
const codePoint = chars[next].codePointAt(0);
if (isVariationSelector(codePoint) || isSkinTone(codePoint)) {
value += chars[next];
next += 1;
continue;
}
if (codePoint === 0x200d && next + 1 < chars.length) {
value += chars[next] + chars[next + 1];
next += 2;
continue;
}
break;
}
return { value, next };
}
function symbolHtml(cluster) {
if (CHESS_LABELS[cluster]) {
return `<span class="symbol symbol-chess" data-symbol="${escapeHtml(CHESS_LABELS[cluster].replaceAll(" ", "-"))}" aria-label="${escapeHtml(CHESS_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
if (CARD_LABELS[cluster]) {
return `<span class="symbol symbol-card" data-symbol="${escapeHtml(CARD_LABELS[cluster])}" aria-label="${escapeHtml(CARD_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
const codePoint = cluster.codePointAt(0);
if ((codePoint >= 0x1f0a0 && codePoint <= 0x1f0ff) || isEmojiBase(codePoint)) {
return `<span class="symbol symbol-emoji">${escapeHtml(cluster)}</span>`;
}
return null;
}
function splitText(value) {
const chars = Array.from(value);
const nodes = [];
let text = "";
function flushText() {
if (text) {
nodes.push({ type: "text", value: text });
text = "";
}
}
for (let index = 0; index < chars.length; ) {
const codePoint = chars[index].codePointAt(0);
const shouldCluster =
CHESS_LABELS[chars[index]] ||
CARD_LABELS[chars[index]] ||
isEmojiBase(codePoint);
if (!shouldCluster) {
text += chars[index];
index += 1;
continue;
}
const { value: cluster, next } = readCluster(chars, index);
const html = symbolHtml(cluster);
if (html) {
flushText();
nodes.push({ type: "html", value: html });
} else {
text += cluster;
}
index = next;
}
flushText();
return nodes;
}
function transformChildren(parent) {
if (!Array.isArray(parent?.children)) return;
const nextChildren = [];
for (const child of parent.children) {
if (child.type === "text") {
nextChildren.push(...splitText(child.value));
continue;
}
transformChildren(child);
nextChildren.push(child);
}
parent.children = nextChildren;
}
export default function remarkSymbols() {
return (tree) => transformChildren(tree);
}

View file

@ -66,7 +66,9 @@
.article-content {
color: hsl(var(--foreground));
font-family: var(--font-heliotrope);
font-family:
var(--font-heliotrope), "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
"Noto Sans Symbols 2", "DejaVu Sans", sans-serif;
font-size: 1.0625rem;
line-height: 1.78;
}
@ -129,6 +131,25 @@
text-underline-offset: 0.18em;
}
.article-content :where(.symbol) {
display: inline-block;
line-height: 1;
vertical-align: -0.08em;
}
.article-content :where(.symbol-emoji) {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", emoji, sans-serif;
}
.article-content :where(.symbol-chess, .symbol-card) {
font-family: "Noto Sans Symbols 2", "DejaVu Sans", "Symbola", serif;
font-size: 1.04em;
}
.article-content :where(.symbol-card[data-symbol="heart"], .symbol-card[data-symbol="diamond"]) {
color: color-mix(in srgb, crimson 80%, hsl(var(--foreground)));
}
.article-content :where(code) {
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;

View file

@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
import remarkSymbols from "./src/remark/symbols.mjs";
import remarkTypography from "./src/remark/typography.mjs";
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
@ -24,6 +25,7 @@ const remarkPlugins = [
remarkMath,
remarkTypography,
remarkCallouts,
remarkSymbols,
];
const rehypePlugins = [rehypeKatex];

View file

@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
["warning", "Warning"],
]);
const CALLOUT_MARKERS = new Map([
["📝", "Note"],
["💡", "Tip"],
["⚡", "Tip"],
["⚠️", "Warning"],
["⚠", "Warning"],
]);
function textFromInline(node) {
if (!node) return "";
if (typeof node.value === "string") return node.value;
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
if (match) {
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
}
const trimmed = first.value.trimStart();
for (const [marker, label] of CALLOUT_MARKERS) {
if (trimmed.startsWith(marker)) {
return label;
}
}
}
if (first?.type !== "strong") return null;
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
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();
return;
}

View file

@ -0,0 +1,160 @@
const CHESS_LABELS = {
"♔": "white king",
"♕": "white queen",
"♖": "white rook",
"♗": "white bishop",
"♘": "white knight",
"♙": "white pawn",
"♚": "black king",
"♛": "black queen",
"♜": "black rook",
"♝": "black bishop",
"♞": "black knight",
"♟": "black pawn",
};
const CARD_LABELS = {
"♠": "spade",
"♤": "spade",
"♣": "club",
"♧": "club",
"♥": "heart",
"♡": "heart",
"♦": "diamond",
"♢": "diamond",
};
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
function isVariationSelector(codePoint) {
return codePoint === 0xfe0e || codePoint === 0xfe0f;
}
function isSkinTone(codePoint) {
return codePoint >= 0x1f3fb && codePoint <= 0x1f3ff;
}
function isEmojiBase(codePoint) {
return (
(codePoint >= 0x1f000 && codePoint <= 0x1faff) ||
(codePoint >= 0x2600 && codePoint <= 0x27bf) ||
codePoint === 0x00a9 ||
codePoint === 0x00ae ||
codePoint === 0x203c ||
codePoint === 0x2049 ||
codePoint === 0x2122 ||
codePoint === 0x2139 ||
codePoint === 0x3030 ||
codePoint === 0x303d ||
codePoint === 0x3297 ||
codePoint === 0x3299
);
}
function readCluster(chars, index) {
let value = chars[index];
let next = index + 1;
while (next < chars.length) {
const codePoint = chars[next].codePointAt(0);
if (isVariationSelector(codePoint) || isSkinTone(codePoint)) {
value += chars[next];
next += 1;
continue;
}
if (codePoint === 0x200d && next + 1 < chars.length) {
value += chars[next] + chars[next + 1];
next += 2;
continue;
}
break;
}
return { value, next };
}
function symbolHtml(cluster) {
if (CHESS_LABELS[cluster]) {
return `<span class="symbol symbol-chess" data-symbol="${escapeHtml(CHESS_LABELS[cluster].replaceAll(" ", "-"))}" aria-label="${escapeHtml(CHESS_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
if (CARD_LABELS[cluster]) {
return `<span class="symbol symbol-card" data-symbol="${escapeHtml(CARD_LABELS[cluster])}" aria-label="${escapeHtml(CARD_LABELS[cluster])}">${escapeHtml(cluster)}</span>`;
}
const codePoint = cluster.codePointAt(0);
if ((codePoint >= 0x1f0a0 && codePoint <= 0x1f0ff) || isEmojiBase(codePoint)) {
return `<span class="symbol symbol-emoji">${escapeHtml(cluster)}</span>`;
}
return null;
}
function splitText(value) {
const chars = Array.from(value);
const nodes = [];
let text = "";
function flushText() {
if (text) {
nodes.push({ type: "text", value: text });
text = "";
}
}
for (let index = 0; index < chars.length; ) {
const codePoint = chars[index].codePointAt(0);
const shouldCluster =
CHESS_LABELS[chars[index]] ||
CARD_LABELS[chars[index]] ||
isEmojiBase(codePoint);
if (!shouldCluster) {
text += chars[index];
index += 1;
continue;
}
const { value: cluster, next } = readCluster(chars, index);
const html = symbolHtml(cluster);
if (html) {
flushText();
nodes.push({ type: "html", value: html });
} else {
text += cluster;
}
index = next;
}
flushText();
return nodes;
}
function transformChildren(parent) {
if (!Array.isArray(parent?.children)) return;
const nextChildren = [];
for (const child of parent.children) {
if (child.type === "text") {
nextChildren.push(...splitText(child.value));
continue;
}
transformChildren(child);
nextChildren.push(child);
}
parent.children = nextChildren;
}
export default function remarkSymbols() {
return (tree) => transformChildren(tree);
}

View file

@ -66,7 +66,9 @@
.article-content {
color: hsl(var(--foreground));
font-family: var(--font-heliotrope);
font-family:
var(--font-heliotrope), "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji",
"Noto Sans Symbols 2", "DejaVu Sans", sans-serif;
font-size: 1.0625rem;
line-height: 1.78;
}
@ -129,6 +131,25 @@
text-underline-offset: 0.18em;
}
.article-content :where(.symbol) {
display: inline-block;
line-height: 1;
vertical-align: -0.08em;
}
.article-content :where(.symbol-emoji) {
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", emoji, sans-serif;
}
.article-content :where(.symbol-chess, .symbol-card) {
font-family: "Noto Sans Symbols 2", "DejaVu Sans", "Symbola", serif;
font-size: 1.04em;
}
.article-content :where(.symbol-card[data-symbol="heart"], .symbol-card[data-symbol="diamond"]) {
color: color-mix(in srgb, crimson 80%, hsl(var(--foreground)));
}
.article-content :where(code) {
border: 1px solid hsl(var(--border));
border-radius: 0.25rem;