-- latex-emoji.lua -- -- @copyright 2020 Takayuki YATO (aka. "ZR") -- GitHub: https://github.com/zr-tex8r -- Twitter: @zr_tex8r -- This program is distributed under the MIT License. -- local filter_name = 'latex-emoji' ---------------------------------------- helpers --- Show debug log? local show_log = true --- The default emoji font local default_emojifont = 'TwemojiMozilla.ttf' --- Use bxcoloremoji package? local bxcoloremoji = false --- The emoji font to use local emojifont, emojifontoptions = nil --- All used codepoints local ucs_used = {} --- The number of emoji text spans. local text_count = 0 local utils = require 'pandoc.utils' local concat, insert, pack, unpack = table.concat, table.insert, table.pack, table.unpack --- Shows a debug log. local function log(fmt, ...) if not show_log then return end io.stderr:write(filter_name..": "..fmt:format(...).."\n") end --- Aborts with an error message. local function abort(fmt, ...) error(filter_name..": "..fmt:format(...)) end --- Returns the Pandoc-or-ordinary type of v. -- @return A string that says type name. local function pantype(v) local t = type(v) return (t == 'table') and v.t or t end --- Makes a comma-separated value string. -- @return A string. local function clist(...) local t, u = pack(...), {} for i = 1, t.n do local v = (t[i] == nil) and '' or tostring(t[i]) if v ~= '' then insert(u, v) end end return concat(u, ',') end --- Makes the sorted sequence of all keys of a given table. -- @return A sequence of strings. local function keys(t) local u = {} for k in pairs(t) do insert(u, k) end table.sort(u) return u end --- Converts a singleton sequence to its element. -- @return The sole element of v if v is a singleton; -- v if v is not a table; otherwise an error is issued. local function tosingle(v, l) if type(v) ~= 'table' then return v end if #v == 1 then return tosingle(v[1], l) end abort("multiple values given: %s", l) end --- Converts a value to a singleton sequence. -- @return The empty table if v is nil; v if v is a table; -- otherwise the singleton of v. local function toseq(v) if v == nil then return {} elseif type(v) == 'table' then return v else return {v} end end --- Converts MetaInlines values inside a MetaValue to strings. -- @return The converted value. (v is not modified.) local function tostring_meta(v, l) if type(v) ~= 'table' then return v end if v.t == 'MetaList' or v.t == nil then local r = {} for k, e in pairs(v) do r[k] = tostring_meta(e, l) end return r elseif v.t == 'MetaInlines' then return utils.stringify(v) else abort("cannot stringify: %s", v.t, l) end end --- Gets the source to go into the header. -- @return LaTeX source string local function get_header() if not bxcoloremoji or not next(ucs_used) then return nil end return ([[ \usepackage[%s]{bxcoloremoji} \newcommand*{\panEmoji}{\coloremoji} ]]):format(clist(emojifont, unpack(emojifontoptions))) end --- Gets the source to go into the head of body. -- @return LaTeX source string local function get_prologue() if bxcoloremoji or not next(ucs_used) then return nil end local fname = emojifont or default_emojifont local fopts = clist('Renderer=HarfBuzz', unpack(emojifontoptions)); local ucs = keys(ucs_used) for i = 1, #ucs do ucs[i] = ('"%X'):format(ucs[i]) end local dcrsrc = concat(ucs, ',\n') return ([[ \makeatletter \ifnum0\ifdefined\directlua\directlua{ if ("\luaescapestring{\luatexbanner}"):match("LuaHBTeX") then tex.write("1") end }\fi>\z@ %% LuaHBTeX is ok \setfontface\p@emoji@font{%s}[%s] \else \@latex@error{You must install a new TeX system (TeX Live 2020)\MessageBreak and then use 'lualatex' engine to print emoji} {The compilation will be aborted.} \let\p@emoji@font\relax \fi \ifdefined\ltjdefcharrange \ltjdefcharrange{208}{ %s} \ltjsetparameter{jacharrange={-208}} \fi \newcommand*{\panEmoji}[1]{{\p@emoji@font#1}} \makeatother ]]):format(fname, fopts, dcrsrc) end --- For debug. local function inspect(v) local t = type(v) if t == 'userdata' or t == 'function' or t == 'nil' then return t elseif t == 'table' then local u, tag = {}, (v.t or 'table') if tag == 'Str' then return tag..'{'..v.text..'}' end for i = 1, #v do u[i] = inspect(v[i]) end return tag..'{'..concat(u, ';')..'}' else return tostring(v) end end ---------------------------------------- phase 'readmeta' --- For Meta elements. local function readmeta_Meta (meta) -- bxcoloremoji if meta.bxcoloremoji == nil then bxcoloremoji = false elseif type(meta.bxcoloremoji) == 'boolean' then bxcoloremoji = meta.bxcoloremoji else abort("not a boolean value: bxcoloremoji") end log('bxcoloremoji = %s', bxcoloremoji) -- emojifont emojifont = tostring_meta(meta.emojifont, "emojifont") emojifont = tosingle(emojifont, "emojifont") log('emojifont = %s', emojifont) -- emojifontoptions emojifontoptions = tostring_meta(meta.emojifontoptions, "emojifontoptions") emojifontoptions = toseq(emojifontoptions) for i in ipairs(emojifontoptions) do emojifontoptions[i] = tosingle(emojifontoptions[i], "emojifontoptions element") log('emojifontoptions = %s', emojifontoptions[i]) end end ---------------------------------------- phase 'mainproc' --- For Span element. local function mainproc_Span(span) if span.classes:includes('emoji', 1) then text_count = text_count + 1 local str = utils.stringify(span.content) for p, uc in utf8.codes(str) do if not ucs_used[uc] and uc >= 0x100 then log("emoji character: U+%04X", uc) ucs_used[uc] = true end end insert(span.content, 1, pandoc.RawInline('latex', [[\panEmoji{]])) insert(span.content, pandoc.RawInline('latex', [[}]])) return span.content end end --- For Meta elements. local function mainproc_Meta(meta) local src = get_header() if src then local headers = meta['header-includes'] if headers == nil then headers = pandoc.MetaList({}) elseif pantype(headers) == 'MetaList' then abort("unexpected metavalue type: header-includes") end insert(headers, pandoc.MetaBlocks{pandoc.RawBlock('latex', src)}) meta['header-includes'] = headers log("header successfully appended") return meta end end --- For the whole document. local function mainproc_Pandoc(doc) log("number of emoji spans: %s", text_count) local src = get_prologue() if src then insert(doc.blocks, 1, pandoc.RawBlock('latex', src)) log("prologue successfully inserted") return doc end end ---------------------------------------- the filter if FORMAT == 'latex' then return { {-- phase 'readmeta' Meta = readmeta_Meta; }; {-- phase 'mainproc' Span = mainproc_Span; Meta = mainproc_Meta; Pandoc = mainproc_Pandoc; }; } else log("format '%s' in not supported", FORMAT) end ---------------------------------------- done