if not modules then modules = { } end modules ['chem-str'] = {
version = 1.001,
comment = "companion to chem-str.mkiv",
author = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
copyright = "PRAGMA ADE / ConTeXt Development Team",
license = "see context related readme files"
}
-- This module in incomplete and experimental.
-- We can push snippets into an mp instance.
local trace_structure = false trackers.register("chemistry.structure", function(v) trace_structure = v end)
local trace_textstack = false trackers.register("chemistry.textstack", function(v) trace_textstack = v end)
local format, gmatch, match, lower, gsub = string.format, string.gmatch, string.match, string.lower, string.gsub
local concat, insert, remove = table.concat, table.insert, table.remove
local apply = structure.processors.apply
local texsprint, ctxcatcodes = tex.sprint, tex.ctxcatcodes
local lpegmatch = lpeg.match
local variables = interfaces.variables
chemicals = chemicals or { }
chemicals.instance = "metafun" -- "ppchtex"
chemicals.format = "metafun"
chemicals.structures = 0
local remapper = {
["+"] = "p",
["-"] = "m",
}
local common_keys = {
b = "line", eb = "line", db = "line", er = "line", dr = "line", br = "line",
sb = "line", msb = "line", psb = "line",
r = "line", pr = "line", mr = "line",
au = "line", ad = "line",
rb = "line", mrb = "line", prb = "line",
rd = "line", mrd = "line", prd = "line",
sr = "line", msr = "line", psr = "line",
c = "line", cc = "line", cd = "line", ccd = "line",
rn = "number", rtn = "number", rbn = "number",
s = "line", ss = "line", pss = "line", mss = "line",
mid = "fixed", mids = "fixed", midz = "text",
z = "text", rz = "text", mrz = "text", prz = "text", crz = "text",
rt = "text", rtt = "text", rbt = "text", zt = "text", zn = "number",
mov = "transform", rot = "transform", adj = "transform", dir = "transform", sub = "transform",
}
local front_keys = {
b = "line", bb= "line",
sb = "line", msb = "line", psb = "line",
r = "line", pr = "line", mr = "line",
z = "text", mrz = "text", prz = "text",
}
local one_keys = {
sb = "line", db = "line", tb = "line",
ep = "line", es = "line", ed = "line", et = "line",
sd = "line", ldd = "line", rdd = "line",
hb = "line", bb = "line", oe = "line", bd = "line", bw = "line",
z = "text", cz = "text", zt = "text", zn = "number",
zbt = "text", zbn = "number", ztt = "text", ztn = "number",
mov = "transform", sub = "transform", dir = "transform", off = "transform",
}
local front_align = {
mrz = { { "b","b","b","b","b","b" } },
prz = { { "t","t","t","t","t","t" } },
}
local syntax = {
one = {
n = 1, max = 8, keys = one_keys,
align = {
z = { { "r", "r_b", "b", "l_b", "l", "l_t", "t", "r_t" } },
--~ z = { { "r", "r", "b", "l", "l", "l", "t", "r" } },
}
},
three = {
n = 3, max = 3, keys = common_keys,
align = {
mrz = { { "r","b","l" }, { "b","l","t" }, { "l","t","r" }, { "t","r","b" } },
rz = { { "r","l_b","l_t" }, { "b","l_t","r_t" }, { "l","r_t","r_b" }, { "t","r_b","l_b" } },
prz = { { "r","l","t" }, { "b","t","r" }, { "l","r","b" }, { "t","b","l" } },
}
},
four = {
n = 4, max = 4, keys = common_keys,
align = {
mrz = { { "t","r","b","l" }, { "r","b","l","t" }, { "b","l","t","r" }, { "l","t","r","b" } },
rz = { { "r_t","r_b","l_b","l_t" }, { "r_b","l_b","l_t","r_t" }, { "l_b","l_t","r_t","r_b" }, { "l_t","r_t","r_b","l_b" } },
prz = { { "r","b","l","t" }, { "b","l","t","r" }, { "l","t","r","b" }, { "t","r","b","l" } },
}
},
five = {
n = 5, max = 5, keys = common_keys,
align = {
mrz = { { "t","r","b","b","l" }, { "r","b","l","l","t" }, { "b","l","t","r","r" }, { "l","t","r","r","b" } },
rz = { { "r","r","b","l","t" }, { "b","b","l","t","r" }, { "l","l","t","r","b" }, { "t","t","r","b","l" } },
prz = { { "r","b","l","t","t" }, { "b","l","t","r","r" }, { "l","t","r","b","b" }, { "t","r","b","l","l" } },
}
},
six = {
n = 6, max = 6, keys = common_keys,
align = {
mrz = { { "t","t","r","b","b","l" }, { "r","b","b","l","t","t" }, { "b","b","l","t","t","r" }, { "l","t","t","r","b","b" } },
rz = { { "r","r","b","l","l","t" }, { "b","b","l","t","t","r" }, { "l","l","t","r","r","b" }, { "t","t","r","b","b","l" } },
prz = { { "r","b","l","l","t","r" }, { "b","l","t","t","r","b" }, { "l","t","r","r","b","l" }, { "t","r","b","b","l","t" } },
}
},
eight = {
n = 8, max = 8, keys = common_keys,
align = { -- todo
mrz = { { "t","r","r","b","b","l","l","t" }, { "r","b","b","l","l","t","t","r" }, { "b","l","l","t","t","r","r","b" }, { "l","t","t","r","r","b","b","l" } },
rz = { { "r","r","b","b","l","l","t","t" }, { "b","b","l","l","t","t","r","r" }, { "l","l","t","t","r","r","b","b" }, { "t","t","r","r","b","b","l","l" } },
prz = { { "r","b","b","l","l","t","t","r" }, { "b","l","l","t","t","r","r","b" }, { "l","t","t","r","r","b","b","l" }, { "t","r","r","b","b","l","l","t" } },
}
},
five_front = {
n = -5, max = 5, keys = front_keys, align = front_align,
},
six_front = {
n = -6, max = 6, keys = front_keys, align = front_align,
},
pb = { direct = 'chem_pb ;' },
pe = { direct = 'chem_pe ;' },
save = { direct = 'chem_save ;' },
restore = { direct = 'chem_restore ;' },
space = { direct = 'chem_symbol("\\chemicalsymbol[space]") ;' },
plus = { direct = 'chem_symbol("\\chemicalsymbol[plus]") ;' },
minus = { direct = 'chem_symbol("\\chemicalsymbol[minus]") ;' },
gives = { direct = 'chem_symbol("\\chemicalsymbol[gives]{%s}{%s}") ;', arguments = 2 },
equilibrium = { direct = 'chem_symbol("\\chemicalsymbol[equilibrium]{%s}{%s}") ;', arguments = 2 },
mesomeric = { direct = 'chem_symbol("\\chemicalsymbol[mesomeric]{%s}{%s}") ;', arguments = 2 },
opencomplex = { direct = 'chem_symbol("\\chemicalsymbol[opencomplex]") ;' },
closecomplex = { direct = 'chem_symbol("\\chemicalsymbol[closecomplex]") ;' },
}
local definitions = { }
function chemicals.undefine(name)
definitions[lower(name)] = nil
end
function chemicals.define(name,spec,text)
name = lower(name)
local dn = definitions[name]
if not dn then dn = { } definitions[name] = dn end
dn[#dn+1] = {
spec = aux.settings_to_array(lower(spec)),
text = aux.settings_to_array(text),
}
end
local metacode, kind, keys, bonds, max, txt, textsize, rot, pstack
local molecule = chemicals.molecule -- or use lpegmatch(chemicals.moleculeparser,...)
local function fetch(txt)
local st = stack[txt]
local t = st.text[st.n]
--~ st.n = st.n + 1
while not t and txt > 1 do
txt = txt - 1
st = stack[txt]
t = st.text[st.n]
--~ st.n = st.n + 1
end
if t then
if trace_textstack then
logs.report("chemical", "fetching from stack %s slot %s: %s",txt,st.n,t)
end
st.n = st.n + 1
end
return txt, t
end
local digit = lpeg.R("09")/tonumber
local colon = lpeg.P(":")
local equal = lpeg.P("=")
local other = 1 - digit - colon - equal
local remapped = lpeg.S("+-") / remapper
local operation = lpeg.Cs((remapped^0 * other)^1)
local amount = digit
local single = digit
local special = (colon * lpeg.C(other^1)) + lpeg.Cc("")
local range = digit * lpeg.P("..") * digit
local set = lpeg.Ct(digit^2)
local text = (equal * lpeg.C(lpeg.P(1)^0)) + lpeg.Cc(false)
local pattern =
(amount + lpeg.Cc(1)) *
operation *
special * (
range * lpeg.Cc(false) * text +
lpeg.Cc(false) * lpeg.Cc(false) * set * text +
single * lpeg.Cc(false) * lpeg.Cc(false) * text +
lpeg.Cc(false) * lpeg.Cc(false) * lpeg.Cc(false) * text
)
--~ local n, operation, index, upto, set, text = lpegmatch(pattern,"RZ1357")
--~ print(lpegmatch(pattern,"RZ=x")) 1 RZ false false false x
--~ print(lpegmatch(pattern,"RZ1=x")) 1 RZ 1 false false x
--~ print(lpegmatch(pattern,"RZ1..3=x")) 1 RZ 1 3 false x
--~ print(lpegmatch(pattern,"RZ13=x")) 1 RZ false false table x
local function process(spec,text,n,rulethickness,rulecolor,offset)
insert(stack,{ spec=spec, text=text, n=n })
local txt = #stack
for i=1,#spec do
local s = spec[i]
local d = definitions[s]
if d then
for i=1,#d do
local di = d[i]
process(di.spec,di.text,1,rulethickness,rulecolor)
end
else
local rep, operation, special, index, upto, set, text = lpegmatch(pattern,s)
if operation == "pb" then
insert(pstack,kind)
metacode[#metacode+1] = syntax.pb.direct
if keys[special] == "text" and index then
if keys["c"..special] == "text" then -- can be option: auto ...
metacode[#metacode+1] = format('chem_c%s(%s,%s,"");',special,bonds,index)
else
metacode[#metacode+1] = format('chem_%s(%s,%s,"");',special,bonds,index)
end
end
elseif operation == "save" then
insert(pstack,kind)
metacode[#metacode+1] = syntax.save.direct
elseif operation == "pe" or operation == "restore" then
kind = remove(pstack)
local ss = syntax[kind]
local prev = bonds or 6
keys, bonds, max, rot = ss.keys, ss.n, ss.max, 1
metacode[#metacode+1] = syntax[operation].direct
metacode[#metacode+1] = format("chem_set(%s,%s) ;",prev,bonds)
elseif operation == "front" then
if syntax[kind .. "_front"] then
kind = kind .. "_front"
local ss = syntax[kind]
local prev = bonds or 6
keys, bonds, max, rot = ss.keys, ss.n, ss.max, 1
metacode[#metacode+1] = format("chem_set(%s,%s) ;",prev,bonds)
end
elseif operation then
local ss = syntax[operation]
if ss then
local ds = ss.direct
if ds then
local sa = ss.arguments
if sa == 1 then
local one ; txt, one = fetch(txt)
metacode[#metacode+1] = format(ds,one or "")
elseif sa ==2 then
local one ; txt, one = fetch(txt)
local two ; txt, two = fetch(txt)
metacode[#metacode+1] = format(ds,one or "",two or "")
else
metacode[#metacode+1] = ds
end
elseif ss.keys then
local prev = bonds or 6
kind, keys, bonds, max, rot = s, ss.keys, ss.n, ss.max, 1
metacode[#metacode+1] = format("chem_set(%s,%s) ;",prev,bonds)
end
else
local what = keys[operation]
if what == "line" then
if set then
for i=1,#set do
local si = set[i]
metacode[#metacode+1] = format("chem_%s(%s,%s,%s,%s,%s);",operation,bonds,si,si,rulethickness,rulecolor)
end
elseif upto then
metacode[#metacode+1] = format("chem_%s(%s,%s,%s,%s,%s);",operation,bonds,index,upto,rulethickness,rulecolor)
elseif index then
metacode[#metacode+1] = format("chem_%s(%s,%s,%s,%s,%s);",operation,bonds,index,index,rulethickness,rulecolor)
else
metacode[#metacode+1] = format("chem_%s(%s,%s,%s,%s,%s);",operation,bonds,1,max,rulethickness,rulecolor)
end
elseif what == "number" then
if set then
for i=1,#set do
local si = set[i]
metacode[#metacode+1] = format('chem_%s(%s,%s,"\\dochemicaltext{%s}");',operation,bonds,si,si)
end
elseif upto then
for i=index,upto do
local si = set[i]
metacode[#metacode+1] = format('chem_%s(%s,%s,"\\dochemicaltext{%s}");',operation,bonds,si,si)
end
elseif index then
metacode[#metacode+1] = format('chem_%s(%s,%s,"\\dochemicaltext{%s}");',operation,bonds,index,index)
else
for i=1,max do
metacode[#metacode+1] = format('chem_%s(%s,%s,"\\dochemicaltext{%s}");',operation,bonds,i,i)
end
end
elseif what == "text" then
local align = syntax[kind].align
align = align and align[operation]
align = align and align[rot]
if set then
for i=1,#set do
local si = set[i]
local t = text
if not t then txt, t = fetch(txt) end
if t then
local a = align and align[si]
if a then a = "." .. a else a = "" end
metacode[#metacode+1] = format('chem_%s%s(%s,%s,"\\dochemicaltext{%s}");',operation,a,bonds,si,molecule(apply(t)))
end
end
elseif upto then
for i=index,upto do
local t = text
if not t then txt, t = fetch(txt) end
if t then
local s = align and align[i]
if s then s = "." .. s else s = "" end
metacode[#metacode+1] = format('chem_%s%s(%s,%s,"\\dochemicaltext{%s}");',operation,s,bonds,i,molecule(apply(t)))
end
end
elseif index == 0 then
local t = text
if not t then txt, t = fetch(txt) end
if t then
metacode[#metacode+1] = format('chem_%s_zero("\\dochemicaltext{%s}");',operation,molecule(apply(t)))
end
elseif index then
local t = text
if not t then txt, t = fetch(txt) end
if t then
local s = align and align[index]
if s then s = "." .. s else s = "" end
metacode[#metacode+1] = format('chem_%s%s(%s,%s,"\\dochemicaltext{%s}");',operation,s,bonds,index,molecule(apply(t)))
end
else
for i=1,max do
local t = text
if not t then txt, t = fetch(txt) end
if t then
local s = align and align[i]
if s then s = "." .. s else s = "" end
metacode[#metacode+1] = format('chem_%s%s(%s,%s,"\\dochemicaltext{%s}");',operation,s,bonds,i,molecule(apply(t)))
end
end
end
elseif what == "transform" then
if index then
for r=1,rep do
metacode[#metacode+1] = format('chem_%s(%s,%s);',operation,bonds,index)
end
if operation == "rot" then
rot = index
end
end
elseif what == "fixed" then
metacode[#metacode+1] = format("chem_%s(%s,%s,%s);",operation,bonds,rulethickness,rulecolor)
end
end
end
end
end
remove(stack)
end
-- the size related values are somewhat special but we want to be
-- compatible
--
-- maybe we should default to fit
--
-- rulethickness in points
function chemicals.start(settings)
chemicals.structures = chemicals.structures + 1
local textsize, rulethickness, rulecolor = settings.size, settings.rulethickness, settings.rulecolor
local width, height, scale, offset = settings.width or 0, settings.height or 0, settings.scale or "medium", settings.offset or 0
local l, r, t, b = settings.left or 0, settings.right or 0, settings.top or 0, settings.bottom or 0
if scale == variables.small then
scale = 500
elseif scale == variables.medium or scale == 0 then
scale = 625
elseif scale == variables.big then
scale = 750
else
scale = tonumber(scale)
if not scale or scale == 0 then
scale = 750
elseif scale < 500 then
scale = 500
end
end
if width == variables.fit then
width = true
else
width = tonumber(width) or 0
if l == 0 then
if r == 0 then
l = (width == 0 and 2000) or width/2
r = l
elseif width ~= 0 then
l = width - r
end
elseif r == 0 and width ~= 0 then
r = width - l
end
width = false
end
if height == variables.fit then
height = true
else
height = tonumber(height) or 0
if t == 0 then
if b == 0 then
t = (height == 0 and 2000) or height/2
b = t
elseif height ~= 0 then
t = height - b
end
elseif b == 0 and height ~= 0 then
b = height - t
end
height = false
end
scale = 0.75 * scale/625
metacode = { format("chem_start_structure(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) ;",
chemicals.structures,
l/25, r/25, t/25, b/25, scale,
tostring(settings.axis == variables.on), tostring(width), tostring(height), tostring(offset)
) }
kind, keys, bonds, stack, rot, pstack = "six", { }, 6, { }, 1, { }
end
function chemicals.stop()
metacode[#metacode+1] = "chem_stop_structure ;"
local mpcode = concat(metacode,"\n")
if trace_structure then
logs.report("chemical", "metapost code:\n%s", mpcode)
end
metapost.graphic(chemicals.instance,chemicals.format,mpcode,"")
metacode = nil
end
function chemicals.component(spec,text,settings)
rulethickness, rulecolor, offset = settings.rulethickness, settings.rulecolor
local spec = aux.settings_to_array(lower(spec))
local text = aux.settings_to_array(text)
metacode[#metacode+1] = "chem_start_component ;"
process(spec,text,1,rulethickness,rulecolor)
metacode[#metacode+1] = "chem_stop_component ;"
end
local inline = {
["single"] = "\\chemicalsinglebond", ["-"] = "\\chemicalsinglebond",
["double"] = "\\chemicaldoublebond", ["--"] = "\\chemicaldoublebond",
["triple"] = "\\chemicaltriplebond", ["---"] = "\\chemicaltriplebond",
["gives"] = "\\chemicalgives", ["->"] = "\\chemicalgives",
["equilibrium"] = "\\chemicalequilibrium", ["<->"] = "\\chemicalequilibrium",
["mesomeric"] = "\\chemicalmesomeric", ["<>"] = "\\chemicalmesomeric",
["plus"] = "\\chemicalsplus", ["+"] = "\\chemicalsplus",
["minus"] = "\\chemicalsminus",
["space"] = "\\chemicalsspace",
}
-- todo: top / bottom
function chemicals.inline(spec)
local spec = aux.settings_to_array(spec)
for i=1,#spec do
local s = spec[i]
local inl = inline[lower(s)]
if inl then
texsprint(ctxcatcodes,inl)
else
texsprint(ctxcatcodes,format("\\chemicalinline{%s}",molecule(s)))
end
end
end
statistics.register("chemical formulas", function()
if chemicals.structures > 0 then
return format("%s chemical structure formulas",chemicals.structures) -- no timing needed, part of metapost
end
end)