This commit is contained in:
parent
6439559017
commit
7aa1fd2372
31 changed files with 1939 additions and 12 deletions
|
|
@ -92,7 +92,9 @@ Use colocated relative assets:
|
||||||
Quarto image attributes such as `{width=70%}` were removed during import. Going
|
Quarto image attributes such as `{width=70%}` were removed during import. Going
|
||||||
forward, use normal Markdown and let Astro CSS handle presentation.
|
forward, use normal Markdown and let Astro CSS handle presentation.
|
||||||
|
|
||||||
Quarto callouts were converted to portable blockquotes:
|
### Callouts
|
||||||
|
|
||||||
|
Write callouts as portable blockquotes. This is the canonical form:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
> **Note**
|
> **Note**
|
||||||
|
|
@ -100,7 +102,7 @@ Quarto callouts were converted to portable blockquotes:
|
||||||
> Text of the callout.
|
> Text of the callout.
|
||||||
```
|
```
|
||||||
|
|
||||||
Use the same convention for new notes:
|
Supported labels are `Note`, `Tip`, `Warning`, `Caution`, and `Aside`:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
> **Warning**
|
> **Warning**
|
||||||
|
|
@ -108,12 +110,78 @@ Use the same convention for new notes:
|
||||||
> This point needs attention.
|
> This point needs attention.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Obsidian-style labels also work:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
> [!tip]
|
||||||
|
> This is a tip.
|
||||||
|
```
|
||||||
|
|
||||||
|
For quick writing, a few leading emoji markers are recognized and stripped:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
> 📝 This becomes a note.
|
||||||
|
> 💡 This becomes a tip.
|
||||||
|
> ⚡ This also becomes a tip.
|
||||||
|
> ⚠️ This becomes a warning.
|
||||||
|
```
|
||||||
|
|
||||||
|
In Astro these render as `<aside class="callout callout-note">`, etc. In the
|
||||||
|
PDF pipeline they render as pastel `tcolorbox` blocks:
|
||||||
|
|
||||||
|
```text
|
||||||
|
note -> blognotebox
|
||||||
|
tip -> blogtipbox
|
||||||
|
warning -> blogwarningbox
|
||||||
|
caution -> blogcautionbox
|
||||||
|
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.
|
||||||
|
|
||||||
Quarto margin notes such as `[text]{.aside}` were converted to:
|
Quarto margin notes such as `[text]{.aside}` were converted to:
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
> **Aside:** text
|
> **Aside:** text
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Emoji And Symbol Text
|
||||||
|
|
||||||
|
Write emoji, chess pieces, and card suits directly in Markdown:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
I like this move: ♘f3. Hearts and diamonds are red: ♥ ♦. Spades and clubs are black: ♠ ♣.
|
||||||
|
This reminder is visible in both outputs: 🎯
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro wraps these glyphs in small semantic spans so they get stable font
|
||||||
|
fallbacks and accessible labels:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<span class="symbol symbol-chess" data-symbol="white-knight" aria-label="white knight">♘</span>
|
||||||
|
<span class="symbol symbol-card" data-symbol="heart" aria-label="heart">♥</span>
|
||||||
|
<span class="symbol symbol-emoji">🎯</span>
|
||||||
|
```
|
||||||
|
|
||||||
|
The PDF pipeline maps the same characters to LaTeX macros:
|
||||||
|
|
||||||
|
```text
|
||||||
|
emoji and emoji sequences -> \BlogEmojiText{...} using the emoji package
|
||||||
|
♔ ♕ ♖ ♗ ♘ ♙ -> \BlogChessWhite{\symking}, etc. using chessfss
|
||||||
|
♚ ♛ ♜ ♝ ♞ ♟ -> \BlogChessBlack{\symking}, etc. using chessfss
|
||||||
|
♥ ♡ ♦ ♢ -> red \heartsuit / \diamondsuit via amssymb + xcolor
|
||||||
|
♠ ♤ ♣ ♧ -> black \spadesuit / \clubsuit via amssymb + xcolor
|
||||||
|
playing-card emoji -> \BlogEmojiText{...}
|
||||||
|
```
|
||||||
|
|
||||||
|
Use Unicode characters in the source. Do not write LaTeX-only symbols in posts
|
||||||
|
unless the post is PDF-only, because raw LaTeX will not generally survive the
|
||||||
|
HTML path.
|
||||||
|
|
||||||
## Semantic Blocks
|
## Semantic Blocks
|
||||||
|
|
||||||
Use Pandoc-style fenced attributes for blocks that need to render differently
|
Use Pandoc-style fenced attributes for blocks that need to render differently
|
||||||
|
|
@ -303,9 +371,18 @@ npm run pdf:post -- sites/research/src/content/research/example/index.md --out /
|
||||||
The command runs:
|
The command runs:
|
||||||
|
|
||||||
- `scripts/pandoc-filters/semantic-blocks.lua` to convert interactive and
|
- `scripts/pandoc-filters/semantic-blocks.lua` to convert interactive and
|
||||||
environment fences.
|
environment fences, internal links, Markdown callouts, emoji, chess pieces,
|
||||||
|
and card suits.
|
||||||
- `scripts/pandoc-templates/semantic-preamble.tex` to load `amsthm`,
|
- `scripts/pandoc-templates/semantic-preamble.tex` to load `amsthm`,
|
||||||
`cleveref`, and the shared theorem definitions.
|
`cleveref`, `tcolorbox`, `emoji`, `chessfss`, and the shared theorem,
|
||||||
|
callout, and symbol definitions.
|
||||||
|
|
||||||
|
Current smoke-test PDFs live outside the repo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/private/tmp/blog-pdf-samples/callout-symbol-test.pdf
|
||||||
|
/private/tmp/blog-pdf-samples/vibes-building-interactives.pdf
|
||||||
|
```
|
||||||
|
|
||||||
The PDF route is intentionally separate from Astro. Astro owns HTML rendering;
|
The PDF route is intentionally separate from Astro. Astro owns HTML rendering;
|
||||||
Pandoc owns PDF rendering. The Markdown conventions above are the shared
|
Pandoc owns PDF rendering. The Markdown conventions above are the shared
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,58 @@ local env_types = {
|
||||||
proof = true,
|
proof = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
traverse = "topdown"
|
||||||
|
|
||||||
|
local callout_labels = {
|
||||||
|
aside = "Aside",
|
||||||
|
caution = "Caution",
|
||||||
|
note = "Note",
|
||||||
|
tip = "Tip",
|
||||||
|
warning = "Warning",
|
||||||
|
}
|
||||||
|
|
||||||
|
local callout_envs = {
|
||||||
|
aside = "blogasidebox",
|
||||||
|
caution = "blogcautionbox",
|
||||||
|
note = "blognotebox",
|
||||||
|
tip = "blogtipbox",
|
||||||
|
warning = "blogwarningbox",
|
||||||
|
}
|
||||||
|
|
||||||
|
local callout_markers = {
|
||||||
|
{ marker = "📝", key = "note" },
|
||||||
|
{ marker = "💡", key = "tip" },
|
||||||
|
{ marker = "⚡", key = "tip" },
|
||||||
|
{ marker = "⚠️", key = "warning" },
|
||||||
|
{ marker = "⚠", key = "warning" },
|
||||||
|
}
|
||||||
|
|
||||||
|
local chess_macros = {
|
||||||
|
["♔"] = "\\BlogChessWhite{\\symking}",
|
||||||
|
["♕"] = "\\BlogChessWhite{\\symqueen}",
|
||||||
|
["♖"] = "\\BlogChessWhite{\\symrook}",
|
||||||
|
["♗"] = "\\BlogChessWhite{\\symbishop}",
|
||||||
|
["♘"] = "\\BlogChessWhite{\\symknight}",
|
||||||
|
["♙"] = "\\BlogChessWhite{\\sympawn}",
|
||||||
|
["♚"] = "\\BlogChessBlack{\\symking}",
|
||||||
|
["♛"] = "\\BlogChessBlack{\\symqueen}",
|
||||||
|
["♜"] = "\\BlogChessBlack{\\symrook}",
|
||||||
|
["♝"] = "\\BlogChessBlack{\\symbishop}",
|
||||||
|
["♞"] = "\\BlogChessBlack{\\symknight}",
|
||||||
|
["♟"] = "\\BlogChessBlack{\\sympawn}",
|
||||||
|
}
|
||||||
|
|
||||||
|
local card_macros = {
|
||||||
|
["♠"] = "\\BlogCardBlack{\\ensuremath{\\spadesuit}}",
|
||||||
|
["♤"] = "\\BlogCardBlack{\\ensuremath{\\spadesuit}}",
|
||||||
|
["♣"] = "\\BlogCardBlack{\\ensuremath{\\clubsuit}}",
|
||||||
|
["♧"] = "\\BlogCardBlack{\\ensuremath{\\clubsuit}}",
|
||||||
|
["♥"] = "\\BlogCardRed{\\ensuremath{\\heartsuit}}",
|
||||||
|
["♡"] = "\\BlogCardRed{\\ensuremath{\\heartsuit}}",
|
||||||
|
["♦"] = "\\BlogCardRed{\\ensuremath{\\diamondsuit}}",
|
||||||
|
["♢"] = "\\BlogCardRed{\\ensuremath{\\diamondsuit}}",
|
||||||
|
}
|
||||||
|
|
||||||
local function has_class(el, class_name)
|
local function has_class(el, class_name)
|
||||||
for _, value in ipairs(el.classes) do
|
for _, value in ipairs(el.classes) do
|
||||||
if value == class_name then
|
if value == class_name then
|
||||||
|
|
@ -27,6 +79,28 @@ local function first_env_class(el)
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function normalize_label(value)
|
||||||
|
value = tostring(value or ""):lower()
|
||||||
|
value = value:gsub("^%s+", ""):gsub("%s+$", "")
|
||||||
|
value = value:gsub("^%[!", ""):gsub("%]$", "")
|
||||||
|
value = value:gsub(":$", "")
|
||||||
|
return value
|
||||||
|
end
|
||||||
|
|
||||||
|
local function inline_text(inline)
|
||||||
|
if not inline then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
return pandoc.utils.stringify(inline)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function first_inline_text(block)
|
||||||
|
if not block or block.t ~= "Para" or not block.content or #block.content == 0 then
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
return inline_text(block.content[1])
|
||||||
|
end
|
||||||
|
|
||||||
local function latex_escape(value)
|
local function latex_escape(value)
|
||||||
value = tostring(value or "")
|
value = tostring(value or "")
|
||||||
value = value:gsub("\\", "\\textbackslash{}")
|
value = value:gsub("\\", "\\textbackslash{}")
|
||||||
|
|
@ -36,6 +110,154 @@ local function latex_escape(value)
|
||||||
return value
|
return value
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function codepoint_at(value, index)
|
||||||
|
return utf8.codepoint(value, index, index)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function codepoint_len(value, index)
|
||||||
|
local byte = string.byte(value, index)
|
||||||
|
if not byte then
|
||||||
|
return 0
|
||||||
|
elseif byte < 0x80 then
|
||||||
|
return 1
|
||||||
|
elseif byte < 0xE0 then
|
||||||
|
return 2
|
||||||
|
elseif byte < 0xF0 then
|
||||||
|
return 3
|
||||||
|
end
|
||||||
|
return 4
|
||||||
|
end
|
||||||
|
|
||||||
|
local function char_at(value, index)
|
||||||
|
local len = codepoint_len(value, index)
|
||||||
|
return value:sub(index, index + len - 1), index + len
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_variation_selector(cp)
|
||||||
|
return cp == 0xFE0E or cp == 0xFE0F
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_skin_tone(cp)
|
||||||
|
return cp >= 0x1F3FB and cp <= 0x1F3FF
|
||||||
|
end
|
||||||
|
|
||||||
|
local function is_emoji_base(cp)
|
||||||
|
return (cp >= 0x1F000 and cp <= 0x1FAFF)
|
||||||
|
or (cp >= 0x2600 and cp <= 0x27BF)
|
||||||
|
or cp == 0x00A9
|
||||||
|
or cp == 0x00AE
|
||||||
|
or cp == 0x203C
|
||||||
|
or cp == 0x2049
|
||||||
|
or cp == 0x2122
|
||||||
|
or cp == 0x2139
|
||||||
|
or cp == 0x3030
|
||||||
|
or cp == 0x303D
|
||||||
|
or cp == 0x3297
|
||||||
|
or cp == 0x3299
|
||||||
|
end
|
||||||
|
|
||||||
|
local function emoji_cluster(value, index)
|
||||||
|
local start = index
|
||||||
|
local current, next_index = char_at(value, index)
|
||||||
|
if current == "" then
|
||||||
|
return "", index
|
||||||
|
end
|
||||||
|
|
||||||
|
index = next_index
|
||||||
|
|
||||||
|
while index <= #value do
|
||||||
|
local cp = codepoint_at(value, index)
|
||||||
|
if is_variation_selector(cp) or is_skin_tone(cp) then
|
||||||
|
local _, ni = char_at(value, index)
|
||||||
|
index = ni
|
||||||
|
elseif cp == 0x200D then
|
||||||
|
local _, ni = char_at(value, index)
|
||||||
|
index = ni
|
||||||
|
if index <= #value then
|
||||||
|
local _, after_joined = char_at(value, index)
|
||||||
|
index = after_joined
|
||||||
|
end
|
||||||
|
else
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return value:sub(start, index - 1), index
|
||||||
|
end
|
||||||
|
|
||||||
|
local function symbol_macro_for_cluster(cluster)
|
||||||
|
local cp = utf8.codepoint(cluster, 1, 1)
|
||||||
|
if not cp then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if chess_macros[cluster] then
|
||||||
|
return chess_macros[cluster]
|
||||||
|
end
|
||||||
|
|
||||||
|
if card_macros[cluster] then
|
||||||
|
return card_macros[cluster]
|
||||||
|
end
|
||||||
|
|
||||||
|
if cp >= 0x1F0A0 and cp <= 0x1F0FF then
|
||||||
|
return "\\BlogEmojiText{" .. cluster .. "}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if is_emoji_base(cp) then
|
||||||
|
return "\\BlogEmojiText{" .. cluster .. "}"
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function symbol_str_to_latex(el)
|
||||||
|
if not FORMAT:match("latex") then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local text = el.text
|
||||||
|
local pieces = pandoc.List()
|
||||||
|
local plain = {}
|
||||||
|
local changed = false
|
||||||
|
local index = 1
|
||||||
|
|
||||||
|
local function flush_plain()
|
||||||
|
if #plain > 0 then
|
||||||
|
pieces:insert(pandoc.Str(table.concat(plain)))
|
||||||
|
plain = {}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
while index <= #text do
|
||||||
|
local cp = codepoint_at(text, index)
|
||||||
|
if cp and (chess_macros[utf8.char(cp)] or card_macros[utf8.char(cp)] or is_emoji_base(cp)) then
|
||||||
|
local cluster, next_index = emoji_cluster(text, index)
|
||||||
|
local macro = symbol_macro_for_cluster(cluster)
|
||||||
|
if macro then
|
||||||
|
flush_plain()
|
||||||
|
pieces:insert(pandoc.RawInline("latex", macro))
|
||||||
|
changed = true
|
||||||
|
index = next_index
|
||||||
|
else
|
||||||
|
local char, ni = char_at(text, index)
|
||||||
|
plain[#plain + 1] = char
|
||||||
|
index = ni
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local char, ni = char_at(text, index)
|
||||||
|
plain[#plain + 1] = char
|
||||||
|
index = ni
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
flush_plain()
|
||||||
|
|
||||||
|
if changed then
|
||||||
|
return pieces
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
local function internal_link_to_cref(el)
|
local function internal_link_to_cref(el)
|
||||||
if FORMAT:match("latex") and el.target:sub(1, 1) == "#" then
|
if FORMAT:match("latex") and el.target:sub(1, 1) == "#" then
|
||||||
local label = el.target:sub(2)
|
local label = el.target:sub(2)
|
||||||
|
|
@ -46,12 +268,24 @@ local function internal_link_to_cref(el)
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function latexize_doc(doc)
|
||||||
|
return doc:walk({
|
||||||
|
Link = internal_link_to_cref,
|
||||||
|
Str = symbol_str_to_latex,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
local function markdown_to_latex(markdown)
|
local function markdown_to_latex(markdown)
|
||||||
local doc = pandoc.read(
|
local doc = pandoc.read(
|
||||||
markdown or "",
|
markdown or "",
|
||||||
"markdown+fenced_code_attributes+tex_math_dollars+tex_math_single_backslash+footnotes+raw_html"
|
"markdown+fenced_code_attributes+tex_math_dollars+tex_math_single_backslash+footnotes+raw_html"
|
||||||
)
|
)
|
||||||
doc = doc:walk({ Link = internal_link_to_cref })
|
doc = latexize_doc(doc)
|
||||||
|
return pandoc.write(doc, "latex")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function blocks_to_latex(blocks)
|
||||||
|
local doc = latexize_doc(pandoc.Pandoc(blocks))
|
||||||
return pandoc.write(doc, "latex")
|
return pandoc.write(doc, "latex")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -88,6 +322,102 @@ local function interactive_to_latex(el)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function remove_first_inline(block)
|
||||||
|
if not block or block.t ~= "Para" or not block.content or #block.content == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
table.remove(block.content, 1)
|
||||||
|
while #block.content > 0 and block.content[1].t == "Space" do
|
||||||
|
table.remove(block.content, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
if #block.content > 0 and block.content[1].t == "Str" then
|
||||||
|
block.content[1].text = block.content[1].text:gsub("^:%s*", "")
|
||||||
|
if block.content[1].text == "" then
|
||||||
|
table.remove(block.content, 1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function strip_text_prefix(block, pattern)
|
||||||
|
if not block or block.t ~= "Para" or not block.content or #block.content == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local first = block.content[1]
|
||||||
|
if first.t ~= "Str" then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
first.text = first.text:gsub(pattern, "")
|
||||||
|
if first.text == "" then
|
||||||
|
remove_first_inline(block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function detect_callout(block)
|
||||||
|
local text = first_inline_text(block)
|
||||||
|
if text == "" then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
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" }
|
||||||
|
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
|
||||||
|
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 }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function callout_to_latex(el)
|
||||||
|
if not FORMAT:match("latex") or not el.content or #el.content == 0 then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local blocks = pandoc.List(el.content)
|
||||||
|
local callout = detect_callout(blocks[1])
|
||||||
|
if not callout then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
if callout.remover == "strong" then
|
||||||
|
remove_first_inline(blocks[1])
|
||||||
|
elseif callout.remover == "bracket" then
|
||||||
|
strip_text_prefix(blocks[1], "^%[![%a-]+%]%s*")
|
||||||
|
elseif callout.remover == "marker" then
|
||||||
|
strip_text_prefix(blocks[1], "^" .. callout.marker .. "%s*")
|
||||||
|
end
|
||||||
|
|
||||||
|
if blocks[1] and blocks[1].t == "Para" and #blocks[1].content == 0 then
|
||||||
|
table.remove(blocks, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local env = callout_envs[callout.key] or "blognotebox"
|
||||||
|
return pandoc.RawBlock(
|
||||||
|
"latex",
|
||||||
|
"\\begin{" .. env .. "}{}\n"
|
||||||
|
.. blocks_to_latex(blocks)
|
||||||
|
.. "\n\\end{" .. env .. "}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
local function env_to_latex(el)
|
local function env_to_latex(el)
|
||||||
local env_type = el.attributes.type or first_env_class(el)
|
local env_type = el.attributes.type or first_env_class(el)
|
||||||
if not env_type or not env_types[env_type] then
|
if not env_type or not env_types[env_type] then
|
||||||
|
|
@ -126,6 +456,14 @@ function CodeBlock(el)
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function BlockQuote(el)
|
||||||
|
return callout_to_latex(el)
|
||||||
|
end
|
||||||
|
|
||||||
function Link(el)
|
function Link(el)
|
||||||
return internal_link_to_cref(el)
|
return internal_link_to_cref(el)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Str(el)
|
||||||
|
return symbol_str_to_latex(el)
|
||||||
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,104 @@
|
||||||
\usepackage{amsmath}
|
\usepackage{amsmath}
|
||||||
\usepackage{amssymb}
|
\usepackage{amssymb}
|
||||||
\usepackage{amsthm}
|
\usepackage{amsthm}
|
||||||
|
\usepackage[most]{tcolorbox}
|
||||||
|
\usepackage{emoji}
|
||||||
|
\usepackage{chessfss}
|
||||||
|
\usepackage{xcolor}
|
||||||
|
|
||||||
|
\definecolor{BlogCalloutNoteBack}{HTML}{FFF8D7}
|
||||||
|
\definecolor{BlogCalloutNoteFrame}{HTML}{D4A72C}
|
||||||
|
\definecolor{BlogCalloutTipBack}{HTML}{E7F6EC}
|
||||||
|
\definecolor{BlogCalloutTipFrame}{HTML}{3A8F64}
|
||||||
|
\definecolor{BlogCalloutWarningBack}{HTML}{FFF0E5}
|
||||||
|
\definecolor{BlogCalloutWarningFrame}{HTML}{C4662D}
|
||||||
|
\definecolor{BlogCalloutCautionBack}{HTML}{FDECEC}
|
||||||
|
\definecolor{BlogCalloutCautionFrame}{HTML}{B94A48}
|
||||||
|
\definecolor{BlogCalloutAsideBack}{HTML}{EEF3FA}
|
||||||
|
\definecolor{BlogCalloutAsideFrame}{HTML}{5D7FA3}
|
||||||
|
|
||||||
|
\tcbset{
|
||||||
|
blog callout/.style={
|
||||||
|
enhanced,
|
||||||
|
breakable,
|
||||||
|
boxrule=0.45pt,
|
||||||
|
leftrule=2.2pt,
|
||||||
|
arc=1.6mm,
|
||||||
|
outer arc=1.6mm,
|
||||||
|
left=7pt,
|
||||||
|
right=7pt,
|
||||||
|
top=6pt,
|
||||||
|
bottom=6pt,
|
||||||
|
before skip=10pt,
|
||||||
|
after skip=10pt,
|
||||||
|
fonttitle=\bfseries\sffamily\small,
|
||||||
|
coltitle=black,
|
||||||
|
attach boxed title to top left={xshift=7pt,yshift=-2.4mm},
|
||||||
|
boxed title style={
|
||||||
|
enhanced,
|
||||||
|
boxrule=0pt,
|
||||||
|
arc=1.2mm,
|
||||||
|
left=5pt,
|
||||||
|
right=5pt,
|
||||||
|
top=2pt,
|
||||||
|
bottom=2pt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
\newtcolorbox{blognotebox}[2][]{
|
||||||
|
blog callout,
|
||||||
|
title={NOTE\if\relax\detokenize{#2}\relax\else: #2\fi},
|
||||||
|
colback=BlogCalloutNoteBack,
|
||||||
|
colframe=BlogCalloutNoteFrame,
|
||||||
|
boxed title style={colback=BlogCalloutNoteBack},
|
||||||
|
#1
|
||||||
|
}
|
||||||
|
|
||||||
|
\newtcolorbox{blogtipbox}[2][]{
|
||||||
|
blog callout,
|
||||||
|
title={TIP\if\relax\detokenize{#2}\relax\else: #2\fi},
|
||||||
|
colback=BlogCalloutTipBack,
|
||||||
|
colframe=BlogCalloutTipFrame,
|
||||||
|
boxed title style={colback=BlogCalloutTipBack},
|
||||||
|
#1
|
||||||
|
}
|
||||||
|
|
||||||
|
\newtcolorbox{blogwarningbox}[2][]{
|
||||||
|
blog callout,
|
||||||
|
title={WARNING\if\relax\detokenize{#2}\relax\else: #2\fi},
|
||||||
|
colback=BlogCalloutWarningBack,
|
||||||
|
colframe=BlogCalloutWarningFrame,
|
||||||
|
boxed title style={colback=BlogCalloutWarningBack},
|
||||||
|
#1
|
||||||
|
}
|
||||||
|
|
||||||
|
\newtcolorbox{blogcautionbox}[2][]{
|
||||||
|
blog callout,
|
||||||
|
title={CAUTION\if\relax\detokenize{#2}\relax\else: #2\fi},
|
||||||
|
colback=BlogCalloutCautionBack,
|
||||||
|
colframe=BlogCalloutCautionFrame,
|
||||||
|
boxed title style={colback=BlogCalloutCautionBack},
|
||||||
|
#1
|
||||||
|
}
|
||||||
|
|
||||||
|
\newtcolorbox{blogasidebox}[2][]{
|
||||||
|
blog callout,
|
||||||
|
title={ASIDE\if\relax\detokenize{#2}\relax\else: #2\fi},
|
||||||
|
colback=BlogCalloutAsideBack,
|
||||||
|
colframe=BlogCalloutAsideFrame,
|
||||||
|
boxed title style={colback=BlogCalloutAsideBack},
|
||||||
|
#1
|
||||||
|
}
|
||||||
|
|
||||||
|
\ExplSyntaxOn
|
||||||
|
\NewDocumentCommand{\BlogEmojiText}{m}{{\emoji_font: #1}}
|
||||||
|
\ExplSyntaxOff
|
||||||
|
|
||||||
|
\newcommand{\BlogChessWhite}[1]{\textcolor{black!55}{#1}}
|
||||||
|
\newcommand{\BlogChessBlack}[1]{\textcolor{black}{#1}}
|
||||||
|
\newcommand{\BlogCardRed}[1]{\textcolor{red!65!black}{#1}}
|
||||||
|
\newcommand{\BlogCardBlack}[1]{\textcolor{black}{#1}}
|
||||||
|
|
||||||
\theoremstyle{plain}
|
\theoremstyle{plain}
|
||||||
\newtheorem{theorem}{Theorem}
|
\newtheorem{theorem}{Theorem}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
|
||||||
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
||||||
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
||||||
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
||||||
|
import remarkSymbols from "./src/remark/symbols.mjs";
|
||||||
import remarkTypography from "./src/remark/typography.mjs";
|
import remarkTypography from "./src/remark/typography.mjs";
|
||||||
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ const remarkPlugins = [
|
||||||
remarkMath,
|
remarkMath,
|
||||||
remarkTypography,
|
remarkTypography,
|
||||||
remarkCallouts,
|
remarkCallouts,
|
||||||
|
remarkSymbols,
|
||||||
];
|
];
|
||||||
const rehypePlugins = [rehypeKatex];
|
const rehypePlugins = [rehypeKatex];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
|
||||||
["warning", "Warning"],
|
["warning", "Warning"],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const CALLOUT_MARKERS = new Map([
|
||||||
|
["📝", "Note"],
|
||||||
|
["💡", "Tip"],
|
||||||
|
["⚡", "Tip"],
|
||||||
|
["⚠️", "Warning"],
|
||||||
|
["⚠", "Warning"],
|
||||||
|
]);
|
||||||
|
|
||||||
function textFromInline(node) {
|
function textFromInline(node) {
|
||||||
if (!node) return "";
|
if (!node) return "";
|
||||||
if (typeof node.value === "string") return node.value;
|
if (typeof node.value === "string") return node.value;
|
||||||
|
|
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
|
||||||
if (match) {
|
if (match) {
|
||||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
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;
|
if (first?.type !== "strong") return null;
|
||||||
|
|
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
|
||||||
const first = paragraph.children?.[0];
|
const first = paragraph.children?.[0];
|
||||||
if (first?.type === "text") {
|
if (first?.type === "text") {
|
||||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
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 (!first.value) paragraph.children.shift();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
160
sites/art/src/remark/symbols.mjs
Normal file
160
sites/art/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 {
|
.article-content {
|
||||||
color: hsl(var(--foreground));
|
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;
|
font-size: 1.0625rem;
|
||||||
line-height: 1.78;
|
line-height: 1.78;
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +131,25 @@
|
||||||
text-underline-offset: 0.18em;
|
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) {
|
.article-content :where(code) {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
|
||||||
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
||||||
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
||||||
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
||||||
|
import remarkSymbols from "./src/remark/symbols.mjs";
|
||||||
import remarkTypography from "./src/remark/typography.mjs";
|
import remarkTypography from "./src/remark/typography.mjs";
|
||||||
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ const remarkPlugins = [
|
||||||
remarkMath,
|
remarkMath,
|
||||||
remarkTypography,
|
remarkTypography,
|
||||||
remarkCallouts,
|
remarkCallouts,
|
||||||
|
remarkSymbols,
|
||||||
];
|
];
|
||||||
const rehypePlugins = [rehypeKatex];
|
const rehypePlugins = [rehypeKatex];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
|
||||||
["warning", "Warning"],
|
["warning", "Warning"],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const CALLOUT_MARKERS = new Map([
|
||||||
|
["📝", "Note"],
|
||||||
|
["💡", "Tip"],
|
||||||
|
["⚡", "Tip"],
|
||||||
|
["⚠️", "Warning"],
|
||||||
|
["⚠", "Warning"],
|
||||||
|
]);
|
||||||
|
|
||||||
function textFromInline(node) {
|
function textFromInline(node) {
|
||||||
if (!node) return "";
|
if (!node) return "";
|
||||||
if (typeof node.value === "string") return node.value;
|
if (typeof node.value === "string") return node.value;
|
||||||
|
|
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
|
||||||
if (match) {
|
if (match) {
|
||||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
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;
|
if (first?.type !== "strong") return null;
|
||||||
|
|
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
|
||||||
const first = paragraph.children?.[0];
|
const first = paragraph.children?.[0];
|
||||||
if (first?.type === "text") {
|
if (first?.type === "text") {
|
||||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
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 (!first.value) paragraph.children.shift();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
160
sites/poetry/src/remark/symbols.mjs
Normal file
160
sites/poetry/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 {
|
.article-content {
|
||||||
color: hsl(var(--foreground));
|
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;
|
font-size: 1.0625rem;
|
||||||
line-height: 1.78;
|
line-height: 1.78;
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +131,25 @@
|
||||||
text-underline-offset: 0.18em;
|
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) {
|
.article-content :where(code) {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
|
||||||
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
||||||
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
||||||
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
||||||
|
import remarkSymbols from "./src/remark/symbols.mjs";
|
||||||
import remarkTypography from "./src/remark/typography.mjs";
|
import remarkTypography from "./src/remark/typography.mjs";
|
||||||
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ const remarkPlugins = [
|
||||||
remarkMath,
|
remarkMath,
|
||||||
remarkTypography,
|
remarkTypography,
|
||||||
remarkCallouts,
|
remarkCallouts,
|
||||||
|
remarkSymbols,
|
||||||
];
|
];
|
||||||
const rehypePlugins = [rehypeKatex];
|
const rehypePlugins = [rehypeKatex];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
|
||||||
["warning", "Warning"],
|
["warning", "Warning"],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const CALLOUT_MARKERS = new Map([
|
||||||
|
["📝", "Note"],
|
||||||
|
["💡", "Tip"],
|
||||||
|
["⚡", "Tip"],
|
||||||
|
["⚠️", "Warning"],
|
||||||
|
["⚠", "Warning"],
|
||||||
|
]);
|
||||||
|
|
||||||
function textFromInline(node) {
|
function textFromInline(node) {
|
||||||
if (!node) return "";
|
if (!node) return "";
|
||||||
if (typeof node.value === "string") return node.value;
|
if (typeof node.value === "string") return node.value;
|
||||||
|
|
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
|
||||||
if (match) {
|
if (match) {
|
||||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
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;
|
if (first?.type !== "strong") return null;
|
||||||
|
|
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
|
||||||
const first = paragraph.children?.[0];
|
const first = paragraph.children?.[0];
|
||||||
if (first?.type === "text") {
|
if (first?.type === "text") {
|
||||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
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 (!first.value) paragraph.children.shift();
|
||||||
return;
|
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);
|
||||||
|
}
|
||||||
|
|
@ -66,7 +66,9 @@
|
||||||
|
|
||||||
.article-content {
|
.article-content {
|
||||||
color: hsl(var(--foreground));
|
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;
|
font-size: 1.0625rem;
|
||||||
line-height: 1.78;
|
line-height: 1.78;
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +131,25 @@
|
||||||
text-underline-offset: 0.18em;
|
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) {
|
.article-content :where(code) {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
|
||||||
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
||||||
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
||||||
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
||||||
|
import remarkSymbols from "./src/remark/symbols.mjs";
|
||||||
import remarkTypography from "./src/remark/typography.mjs";
|
import remarkTypography from "./src/remark/typography.mjs";
|
||||||
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ const remarkPlugins = [
|
||||||
remarkMath,
|
remarkMath,
|
||||||
remarkTypography,
|
remarkTypography,
|
||||||
remarkCallouts,
|
remarkCallouts,
|
||||||
|
remarkSymbols,
|
||||||
];
|
];
|
||||||
const rehypePlugins = [rehypeKatex];
|
const rehypePlugins = [rehypeKatex];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
|
||||||
["warning", "Warning"],
|
["warning", "Warning"],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const CALLOUT_MARKERS = new Map([
|
||||||
|
["📝", "Note"],
|
||||||
|
["💡", "Tip"],
|
||||||
|
["⚡", "Tip"],
|
||||||
|
["⚠️", "Warning"],
|
||||||
|
["⚠", "Warning"],
|
||||||
|
]);
|
||||||
|
|
||||||
function textFromInline(node) {
|
function textFromInline(node) {
|
||||||
if (!node) return "";
|
if (!node) return "";
|
||||||
if (typeof node.value === "string") return node.value;
|
if (typeof node.value === "string") return node.value;
|
||||||
|
|
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
|
||||||
if (match) {
|
if (match) {
|
||||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
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;
|
if (first?.type !== "strong") return null;
|
||||||
|
|
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
|
||||||
const first = paragraph.children?.[0];
|
const first = paragraph.children?.[0];
|
||||||
if (first?.type === "text") {
|
if (first?.type === "text") {
|
||||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
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 (!first.value) paragraph.children.shift();
|
||||||
return;
|
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 {
|
.article-content {
|
||||||
color: hsl(var(--foreground));
|
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;
|
font-size: 1.0625rem;
|
||||||
line-height: 1.78;
|
line-height: 1.78;
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +131,25 @@
|
||||||
text-underline-offset: 0.18em;
|
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) {
|
.article-content :where(code) {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
|
||||||
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
||||||
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
||||||
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
||||||
|
import remarkSymbols from "./src/remark/symbols.mjs";
|
||||||
import remarkTypography from "./src/remark/typography.mjs";
|
import remarkTypography from "./src/remark/typography.mjs";
|
||||||
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ const remarkPlugins = [
|
||||||
remarkMath,
|
remarkMath,
|
||||||
remarkTypography,
|
remarkTypography,
|
||||||
remarkCallouts,
|
remarkCallouts,
|
||||||
|
remarkSymbols,
|
||||||
];
|
];
|
||||||
const rehypePlugins = [rehypeKatex];
|
const rehypePlugins = [rehypeKatex];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
|
||||||
["warning", "Warning"],
|
["warning", "Warning"],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const CALLOUT_MARKERS = new Map([
|
||||||
|
["📝", "Note"],
|
||||||
|
["💡", "Tip"],
|
||||||
|
["⚡", "Tip"],
|
||||||
|
["⚠️", "Warning"],
|
||||||
|
["⚠", "Warning"],
|
||||||
|
]);
|
||||||
|
|
||||||
function textFromInline(node) {
|
function textFromInline(node) {
|
||||||
if (!node) return "";
|
if (!node) return "";
|
||||||
if (typeof node.value === "string") return node.value;
|
if (typeof node.value === "string") return node.value;
|
||||||
|
|
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
|
||||||
if (match) {
|
if (match) {
|
||||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
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;
|
if (first?.type !== "strong") return null;
|
||||||
|
|
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
|
||||||
const first = paragraph.children?.[0];
|
const first = paragraph.children?.[0];
|
||||||
if (first?.type === "text") {
|
if (first?.type === "text") {
|
||||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
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 (!first.value) paragraph.children.shift();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
160
sites/research/src/remark/symbols.mjs
Normal file
160
sites/research/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 {
|
.article-content {
|
||||||
color: hsl(var(--foreground));
|
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;
|
font-size: 1.0625rem;
|
||||||
line-height: 1.78;
|
line-height: 1.78;
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +131,25 @@
|
||||||
text-underline-offset: 0.18em;
|
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) {
|
.article-content :where(code) {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
|
||||||
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
||||||
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
||||||
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
||||||
|
import remarkSymbols from "./src/remark/symbols.mjs";
|
||||||
import remarkTypography from "./src/remark/typography.mjs";
|
import remarkTypography from "./src/remark/typography.mjs";
|
||||||
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ const remarkPlugins = [
|
||||||
remarkMath,
|
remarkMath,
|
||||||
remarkTypography,
|
remarkTypography,
|
||||||
remarkCallouts,
|
remarkCallouts,
|
||||||
|
remarkSymbols,
|
||||||
];
|
];
|
||||||
const rehypePlugins = [rehypeKatex];
|
const rehypePlugins = [rehypeKatex];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
|
||||||
["warning", "Warning"],
|
["warning", "Warning"],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const CALLOUT_MARKERS = new Map([
|
||||||
|
["📝", "Note"],
|
||||||
|
["💡", "Tip"],
|
||||||
|
["⚡", "Tip"],
|
||||||
|
["⚠️", "Warning"],
|
||||||
|
["⚠", "Warning"],
|
||||||
|
]);
|
||||||
|
|
||||||
function textFromInline(node) {
|
function textFromInline(node) {
|
||||||
if (!node) return "";
|
if (!node) return "";
|
||||||
if (typeof node.value === "string") return node.value;
|
if (typeof node.value === "string") return node.value;
|
||||||
|
|
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
|
||||||
if (match) {
|
if (match) {
|
||||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
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;
|
if (first?.type !== "strong") return null;
|
||||||
|
|
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
|
||||||
const first = paragraph.children?.[0];
|
const first = paragraph.children?.[0];
|
||||||
if (first?.type === "text") {
|
if (first?.type === "text") {
|
||||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
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 (!first.value) paragraph.children.shift();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
160
sites/reviews/src/remark/symbols.mjs
Normal file
160
sites/reviews/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 {
|
.article-content {
|
||||||
color: hsl(var(--foreground));
|
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;
|
font-size: 1.0625rem;
|
||||||
line-height: 1.78;
|
line-height: 1.78;
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +131,25 @@
|
||||||
text-underline-offset: 0.18em;
|
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) {
|
.article-content :where(code) {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import remarkCallouts from "./src/remark/callouts.mjs";
|
||||||
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs";
|
||||||
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs";
|
||||||
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs";
|
||||||
|
import remarkSymbols from "./src/remark/symbols.mjs";
|
||||||
import remarkTypography from "./src/remark/typography.mjs";
|
import remarkTypography from "./src/remark/typography.mjs";
|
||||||
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
import { ACTIVE_BLOG_SITE } from "./src/blog-sites.js";
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ const remarkPlugins = [
|
||||||
remarkMath,
|
remarkMath,
|
||||||
remarkTypography,
|
remarkTypography,
|
||||||
remarkCallouts,
|
remarkCallouts,
|
||||||
|
remarkSymbols,
|
||||||
];
|
];
|
||||||
const rehypePlugins = [rehypeKatex];
|
const rehypePlugins = [rehypeKatex];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@ const CALLOUT_LABELS = new Map([
|
||||||
["warning", "Warning"],
|
["warning", "Warning"],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const CALLOUT_MARKERS = new Map([
|
||||||
|
["📝", "Note"],
|
||||||
|
["💡", "Tip"],
|
||||||
|
["⚡", "Tip"],
|
||||||
|
["⚠️", "Warning"],
|
||||||
|
["⚠", "Warning"],
|
||||||
|
]);
|
||||||
|
|
||||||
function textFromInline(node) {
|
function textFromInline(node) {
|
||||||
if (!node) return "";
|
if (!node) return "";
|
||||||
if (typeof node.value === "string") return node.value;
|
if (typeof node.value === "string") return node.value;
|
||||||
|
|
@ -30,6 +38,13 @@ function paragraphCalloutLabel(paragraph) {
|
||||||
if (match) {
|
if (match) {
|
||||||
return CALLOUT_LABELS.get(normalizeLabel(match[1]));
|
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;
|
if (first?.type !== "strong") return null;
|
||||||
|
|
@ -46,6 +61,10 @@ function removeLeadingLabel(paragraph, label) {
|
||||||
const first = paragraph.children?.[0];
|
const first = paragraph.children?.[0];
|
||||||
if (first?.type === "text") {
|
if (first?.type === "text") {
|
||||||
first.value = first.value.replace(/^\s*\[!(aside|caution|note|tip|warning)\]\s*/i, "");
|
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 (!first.value) paragraph.children.shift();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
160
sites/vibes/src/remark/symbols.mjs
Normal file
160
sites/vibes/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 {
|
.article-content {
|
||||||
color: hsl(var(--foreground));
|
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;
|
font-size: 1.0625rem;
|
||||||
line-height: 1.78;
|
line-height: 1.78;
|
||||||
}
|
}
|
||||||
|
|
@ -129,6 +131,25 @@
|
||||||
text-underline-offset: 0.18em;
|
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) {
|
.article-content :where(code) {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.25rem;
|
border-radius: 0.25rem;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue