391 lines
12 KiB
Lua
391 lines
12 KiB
Lua
-- this is essentially
|
|
-- https://github.com/jupyter/nbconvert/blob/main/nbconvert/filters/ansi.py
|
|
-- converted to lua
|
|
|
|
-- good list of ANSI escape sequences:
|
|
-- https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
|
|
|
|
|
|
local ANSI_COLORS = {
|
|
"ansi-black",
|
|
"ansi-red",
|
|
"ansi-green",
|
|
"ansi-yellow",
|
|
"ansi-blue",
|
|
"ansi-magenta",
|
|
"ansi-cyan",
|
|
"ansi-white",
|
|
"ansi-black-intense",
|
|
"ansi-red-intense",
|
|
"ansi-green-intense",
|
|
"ansi-yellow-intense",
|
|
"ansi-blue-intense",
|
|
"ansi-magenta-intense",
|
|
"ansi-cyan-intense",
|
|
"ansi-white-intense"
|
|
}
|
|
|
|
local function get_extended_color(numbers)
|
|
local n = table.remove(numbers, 1)
|
|
local r,g,b,idx
|
|
if n == 2 and #numbers >=3 then
|
|
-- 24bit RGB
|
|
r = table.remove(numbers, 1)
|
|
g = table.remove(numbers, 1)
|
|
b = table.remove(numbers, 1)
|
|
elseif n == 5 and #numbers >= 1 then
|
|
-- 256 colors
|
|
idx = table.remove(numbers, 1)
|
|
if idx < 16 then
|
|
-- 16 default terminal colors
|
|
return idx
|
|
elseif idx < 232 then
|
|
-- 6x6x6 color cube, see http://stackoverflow.com/a/27165165/500098
|
|
r = (idx - 16) // 36
|
|
r = 55 + r * 40
|
|
if r < 0 then r = 0 end
|
|
g = ((idx - 16) % 36) // 6
|
|
g = 55 + g * 40
|
|
if g < 0 then g = 0 end
|
|
b = (idx - 16) % 6
|
|
b = 55 + b * 40
|
|
if b < 0 then b = 0 end
|
|
elseif idx < 256 then
|
|
-- grayscale, see http://stackoverflow.com/a/27165165/500098
|
|
r = (idx - 232) * 10 + 8
|
|
g = r
|
|
b = r
|
|
end
|
|
end
|
|
return {r, g, b}
|
|
end
|
|
|
|
|
|
--[=[
|
|
local re = require "re"
|
|
|
|
local ANSI = re.compile [[
|
|
'\x1b%[' {.*?} {[@-~]}
|
|
]]
|
|
|
|
--]=]
|
|
|
|
local function LaTeXconverter(fg, bg, bold, underline, inverse)
|
|
if not (fg or bg or bold or underline or inverse) then
|
|
return "",""
|
|
end
|
|
|
|
local starttag = ""
|
|
local endtag = ""
|
|
|
|
if inverse then
|
|
fg, bg = bg, fg
|
|
end
|
|
|
|
if type(fg) == "number" then
|
|
starttag = starttag .. [[\textcolor{]] .. ANSI_COLORS[fg+1] .. "}{"
|
|
endtag = "}" .. endtag
|
|
elseif type(fg) == "table" then
|
|
-- See http://tex.stackexchange.com/a/291102/13684
|
|
starttag = starttag .. [[\def\tcRGB{\textcolor[RGB]}\expandafter]]
|
|
starttag = starttag .. string.format([[\tcRGB\expandafter{\detokenize{%d,%d,%d}}{]], fg[1], fg[2], fg[3])
|
|
endtag = "}" .. endtag
|
|
elseif inverse then
|
|
starttag = starttag .. [[\textcolor{ansi-default-inverse-fg}{]]
|
|
endtag = "}" .. endtag
|
|
end
|
|
|
|
if type(bg) == "number" then
|
|
starttag = starttag .. [[\setlength{\fboxsep}{0pt}]]
|
|
starttag = starttag .. [[\colorbox{]] .. ANSI_COLORS[bg+1] .. "}{"
|
|
endtag = [[\strut}]] .. endtag
|
|
elseif type(bg) == "table" then
|
|
-- See http://tex.stackexchange.com/a/291102/13684
|
|
starttag = starttag .. [[\setlength{\fboxsep}{0pt}]]
|
|
starttag = starttag .. [[\def\cbRGB{\colorbox[RGB]}\expandafter]]
|
|
starttag = starttag .. string.format([[\cbRGB\expandafter{\detokenize{%d,%d,%d}}{]], bg[1], bg[2], bg[3])
|
|
endtag = [[\strut}]] .. endtag
|
|
elseif inverse then
|
|
starttag = starttag .. [[\setlength{\fboxsep}{0pt}]]
|
|
starttag = starttag .. [[\colorbox{ansi-default-inverse-bg}{]]
|
|
endtag = [[\strut}]] .. endtag
|
|
end
|
|
|
|
if bold then
|
|
starttag = starttag .. [[\textbf{]]
|
|
endtag = "}" .. endtag
|
|
end
|
|
|
|
if underline then
|
|
starttag = starttag .. [[\underline{]]
|
|
endtag = "}" .. endtag
|
|
end
|
|
return starttag, endtag
|
|
end
|
|
|
|
local function HTMLconverter(fg, bg, bold, underline, inverse)
|
|
if not (fg or bg or bold or underline or inverse) then
|
|
return "",""
|
|
end
|
|
local classes = {}
|
|
local styles = {}
|
|
local type = type -- more efficient?
|
|
local next = next
|
|
if inverse then
|
|
fg, bg = bg, fg
|
|
end
|
|
|
|
if type(fg) == "number" then
|
|
table.insert(classes, ANSI_COLORS[fg+1] .. "-fg")
|
|
elseif type(fg) == "table" then
|
|
table.insert(styles, string.format("color: rgb(%d,%d,%d)", fg[1], fg[2], fg[3]))
|
|
elseif inverse then
|
|
table.insert(classes, "ansi-default-inverse-fg")
|
|
end
|
|
|
|
if type(bg) == "number" then
|
|
table.insert(classes, ANSI_COLORS[bg+1] .. "-bg")
|
|
elseif type(bg) == "table" then
|
|
table.insert(styles, string.format("background-color: rgb(%d,%d,%d)",
|
|
bg[1], bg[2], bg[3]))
|
|
elseif inverse then
|
|
table.insert(classes, "ansi-default-inverse-bg")
|
|
end
|
|
|
|
if bold then
|
|
table.insert(classes, "ansi-bold")
|
|
end
|
|
|
|
if underline then
|
|
table.insert(classes, "ansi-underline")
|
|
end
|
|
|
|
local starttag = "<span"
|
|
if next(classes) ~= nil then
|
|
starttag = starttag .. ' class="' .. table.concat(classes, " ") .. '"'
|
|
end
|
|
|
|
if next(styles) ~= nil then
|
|
starttag = starttag .. ' style="' .. table.concat(styles, " ") .. '"'
|
|
end
|
|
|
|
return starttag..">","</span>"
|
|
end
|
|
|
|
|
|
local function codeBlockTrans(e)
|
|
local converter, fmt
|
|
if quarto.doc.isFormat('latex') then
|
|
converter = LaTeXconverter
|
|
fmt = 'latex'
|
|
elseif quarto.doc.isFormat('html') then
|
|
converter = HTMLconverter
|
|
fmt = 'html'
|
|
else
|
|
return
|
|
end
|
|
|
|
-- not for input cells
|
|
if e.classes:includes("julia") or e.classes:includes("cell-code") then
|
|
return
|
|
end
|
|
|
|
if #e.classes > 0 and not e.classes:includes("julia-stderr") then
|
|
return
|
|
end
|
|
|
|
local texenv="OutputCell"
|
|
local codeclass=""
|
|
if string.find(e.text, "\u{a35f}\u{2983}") then
|
|
texenv = "AnsiOutputCell"
|
|
codeclass = "ansi"
|
|
end
|
|
if e.classes:includes("julia-stderr") then
|
|
texenv = "StderrOutputCell"
|
|
codeclass = codeclass .. " julia-stderr" -- empty leading space doesn't matter
|
|
end
|
|
|
|
local out=""
|
|
-- if string.find(e.text, "\x1b%[") then
|
|
if string.find(e.text, "\u{a35f}\u{2983}") then
|
|
local bold = false
|
|
local underline = false
|
|
local inverse = false
|
|
local text = e.text
|
|
local chunk = ""
|
|
local fg = nil
|
|
local bg = nil
|
|
local starttag = ""
|
|
local endtag = ""
|
|
local numbers={}
|
|
|
|
while text ~= "" do
|
|
numbers = {}
|
|
-- local s1, e1, c1, d1 = string.find(text, "\x1b%[(.-)([@-~])")
|
|
local s1, e1, c1, d1 = string.find(text, "\u{a35f}\u{2983}(.-)([@-~])")
|
|
if s1 then
|
|
if d1 == "m" then
|
|
for i in string.gmatch(c1, "([^;]*)") do
|
|
table.insert(numbers, tonumber(i))
|
|
end
|
|
else
|
|
quarto.log.warning("Unsupported ANSI sequence ESC["..c1..d1.." ignored\n" )
|
|
end
|
|
chunk, text = text:sub(1, s1-1), text:sub(e1+1)
|
|
else
|
|
chunk, text = text, ""
|
|
end
|
|
|
|
if chunk ~= "" then
|
|
if bold and type(fg)=="number" and fg<8 then
|
|
starttag, endtag = converter(fg+8, bg, bold, underline, inverse)
|
|
else
|
|
starttag, endtag = converter(fg, bg, bold, underline, inverse)
|
|
end
|
|
out = out .. starttag .. chunk .. endtag
|
|
end
|
|
|
|
while next(numbers) ~= nil do
|
|
local n = table.remove(numbers, 1)
|
|
if n == 0 then
|
|
fg = nil
|
|
bg = nil
|
|
bold = false
|
|
inverse = false
|
|
underline = false
|
|
elseif n == 1 then
|
|
bold = true
|
|
elseif n == 4 then
|
|
underline = true
|
|
elseif n == 5 then
|
|
bold = true -- 'blinking'
|
|
elseif n == 7 then
|
|
inverse = true
|
|
elseif n == 21 or n == 22 then
|
|
bold = false
|
|
elseif n == 24 then
|
|
underline = false
|
|
elseif n == 27 then
|
|
inverse = false
|
|
elseif n >= 30 and n <= 37 then
|
|
fg = n - 30
|
|
elseif n == 38 then
|
|
fg = get_extended_color(numbers)
|
|
elseif n == 39 then
|
|
fg = nil
|
|
elseif n >= 40 and n <= 47 then
|
|
bg = n - 40
|
|
elseif n == 48 then
|
|
bg = get_extended_color(numbers)
|
|
elseif n == 49 then
|
|
bg = nil
|
|
elseif n >= 90 and n <= 97 then
|
|
fg = n + 8 - 90
|
|
elseif n >= 100 and n <= 107 then
|
|
bg = n + 8 - 100
|
|
else
|
|
quarto.log.warning(string.format("ESC sequence with unknown code %d before:\n",n))
|
|
quarto.log.warning(chunk.."\n")
|
|
end
|
|
end
|
|
end
|
|
else
|
|
out = e.text
|
|
end
|
|
if fmt == 'html' then
|
|
return pandoc.RawBlock(fmt,
|
|
'<pre class="' .. codeclass ..'"><code class="' .. codeclass .. ' ansi">'..out..'</code></pre>')
|
|
end
|
|
if fmt == 'latex' then
|
|
return pandoc.RawBlock(fmt, [[\begin{]]..texenv.."}\n"..out.."\n"..[[\end{]].. texenv .. "}")
|
|
end
|
|
|
|
end
|
|
|
|
-- if div has class 'cell-output-stderr', give CodeBlocks in this div the class 'julia-stderr'
|
|
local function divStderr(e)
|
|
if e.classes:includes("cell-output-stderr") then
|
|
local c = e.content
|
|
for i,el in pairs(c) do
|
|
if el.t == 'CodeBlock' then
|
|
el.classes:insert("julia-stderr")
|
|
end
|
|
end
|
|
return e
|
|
end
|
|
end
|
|
|
|
-- repair julia ? output
|
|
local function divCodeBlockNoHeader1(e)
|
|
if not e.classes:includes("cell-output") then
|
|
return
|
|
end
|
|
local c = e.content
|
|
for i, el in pairs(c) do
|
|
if el.t == 'Header' then
|
|
el.level = 6
|
|
-- elneu = pandoc.Para(el.content)
|
|
-- c[i] = elneu
|
|
end
|
|
if el.t == 'CodeBlock' then
|
|
if el.classes:includes("jldoctest") then
|
|
x,i = el.classes:find("jldoctest")
|
|
el.classes:remove(i)
|
|
end
|
|
end
|
|
end
|
|
return e
|
|
end
|
|
|
|
-- test if two divs should be merged
|
|
local function testmerge(d1, d2)
|
|
return d1 and d1.t == "Div" and d1.classes:includes("cell-output") and #d1.content == 1
|
|
and d2 and d2.t == "Div" and d2.classes:includes("cell-output") and #d2.content == 1
|
|
and d1.content[1].t == "CodeBlock" and not d1.classes:includes("cell-output-stderr")
|
|
and d2.content[1].t == "CodeBlock" and not d2.classes:includes("cell-output-stderr")
|
|
end
|
|
|
|
-- merge (div (codecell (text1)), div (codecell(text2))) to div(codecell(text1+text2))
|
|
local function blockMerge(es)
|
|
local nl = ""
|
|
for i = #es-1, 1, -1 do
|
|
if testmerge(es[i], es[i+1]) then
|
|
str1 = es[i].content[1].text
|
|
str2 = es[i+1].content[1].text
|
|
nl = "\n"
|
|
if es[i].classes:includes("cell-output-stdout") and es[i+1].classes:includes("cell-output-stdout") then
|
|
if str1:sub(-1) == "\n" then
|
|
nl = ""
|
|
end
|
|
if str2:sub(1, 1) == "\n" then
|
|
nl = ""
|
|
end
|
|
end
|
|
es[i].content[1].text = str1 .. nl .. str2
|
|
es:remove(i+1)
|
|
end
|
|
end
|
|
return es
|
|
end
|
|
|
|
local function metaAdd(meta)
|
|
--for key, val in pairs(PANDOC_READER_OPTIONS) do
|
|
-- quarto.log.warning(key, val)
|
|
--end
|
|
quarto.doc.addHtmlDependency({name='ansicolors',
|
|
stylesheets = {'resources/css/ansicolor.css'}})
|
|
quarto.doc.addHtmlDependency({name='juliamonofont',
|
|
stylesheets = {'resources/css/juliamono.css'}})
|
|
if quarto.doc.isFormat('latex') then
|
|
quarto.doc.include_file("in-header", "resources/juliainc.tex")
|
|
end
|
|
end
|
|
|
|
return {
|
|
{Div = divStderr},
|
|
{Div = divCodeBlockNoHeader1},
|
|
{Blocks = blockMerge},
|
|
{CodeBlock = codeBlockTrans},
|
|
{Meta = metaAdd}
|
|
}
|