This commit is contained in:
parent
6439559017
commit
7aa1fd2372
31 changed files with 1939 additions and 12 deletions
|
|
@ -9,6 +9,58 @@ local env_types = {
|
|||
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)
|
||||
for _, value in ipairs(el.classes) do
|
||||
if value == class_name then
|
||||
|
|
@ -27,6 +79,28 @@ local function first_env_class(el)
|
|||
return nil
|
||||
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)
|
||||
value = tostring(value or "")
|
||||
value = value:gsub("\\", "\\textbackslash{}")
|
||||
|
|
@ -36,6 +110,154 @@ local function latex_escape(value)
|
|||
return value
|
||||
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)
|
||||
if FORMAT:match("latex") and el.target:sub(1, 1) == "#" then
|
||||
local label = el.target:sub(2)
|
||||
|
|
@ -46,12 +268,24 @@ local function internal_link_to_cref(el)
|
|||
return nil
|
||||
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 doc = pandoc.read(
|
||||
markdown or "",
|
||||
"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")
|
||||
end
|
||||
|
||||
|
|
@ -88,6 +322,102 @@ local function interactive_to_latex(el)
|
|||
)
|
||||
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 env_type = el.attributes.type or first_env_class(el)
|
||||
if not env_type or not env_types[env_type] then
|
||||
|
|
@ -126,6 +456,14 @@ function CodeBlock(el)
|
|||
return nil
|
||||
end
|
||||
|
||||
function BlockQuote(el)
|
||||
return callout_to_latex(el)
|
||||
end
|
||||
|
||||
function Link(el)
|
||||
return internal_link_to_cref(el)
|
||||
end
|
||||
|
||||
function Str(el)
|
||||
return symbol_str_to_latex(el)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,104 @@
|
|||
\usepackage{amsmath}
|
||||
\usepackage{amssymb}
|
||||
\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}
|
||||
\newtheorem{theorem}{Theorem}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue