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/puzzles/src/remark/symbols.mjs
Normal file
160
sites/puzzles/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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue