This commit is contained in:
parent
6439559017
commit
7aa1fd2372
31 changed files with 1939 additions and 12 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
160
sites/reflections/src/remark/symbols.mjs
Normal file
160
sites/reflections/src/remark/symbols.mjs
Normal 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("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue