chem-str.lua /size: 35 Kb    last modification: 2023-12-21 09:44
1if not modules then modules = { } end modules ['chem-str'] = {
2    version   = 1.001,
3    comment   = "companion to chem-str.mkiv",
4    author    = "Hans Hagen and Alan Braslau",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- The original \PPCHTEX\ code was written in pure \TEX\, although later we made
10-- the move from \PICTEX\ to \METAPOST\. The current implementation is a mix between
11-- \TEX\, \LUA\ and \METAPOST. Although the first objective is to get a compatible
12-- but better implementation, later versions might provide more.
13--
14-- Well, the later version has arrived as Alan took it upon him to make the code
15-- deviate even further from the original implementation. The original (early \MKII)
16-- variant operated within the boundaries of \PICTEX\ and as it supported MetaPost as
17-- alternative output. As a consequence it still used a stepwise graphic construction
18-- approach. As we used \TEX\ for parsing, the syntax was more rigid than it is now.
19-- This new variant uses a more mathematical and metapostisch approach. In the process
20-- more rendering variants have been added and alignment has been automated. As a result
21-- the current user interface is slightly different from the old one but hopefully users
22-- will like the added value.
23
24-- directive_strictorder: one might set this to off when associated texts are disordered too
25
26local trace_structure       = false  trackers  .register("chemistry.structure",     function(v) trace_structure       = v end)
27local trace_metapost        = false  trackers  .register("chemistry.metapost",      function(v) trace_metapost        = v end)
28local trace_boundingbox     = false  trackers  .register("chemistry.boundingbox",   function(v) trace_boundingbox     = v end)
29local trace_textstack       = false  trackers  .register("chemistry.textstack",     function(v) trace_textstack       = v end)
30local directive_strictorder = true   directives.register("chemistry.strictorder",   function(v) directive_strictorder = v end)
31local directive_strictindex = false  directives.register("chemistry.strictindex",   function(v) directive_strictindex = v end)
32
33local report_chemistry = logs.reporter("chemistry")
34
35local tonumber = tonumber
36local format, gmatch, match, lower, gsub = string.format, string.gmatch, string.match, string.lower, string.gsub
37local concat, insert, remove, unique, sorted = table.concat, table.insert, table.remove, table.unique, table.sorted
38local processor_tostring = typesetters and typesetters.processors.tostring
39local settings_to_array = utilities.parsers.settings_to_array
40local settings_to_array_with_repeat = utilities.parsers.settings_to_array_with_repeat
41
42local lpegmatch, lpegpatterns = lpeg.match, lpeg.patterns
43local P, R, S, C, Cs, Ct, Cc, Cmt = lpeg.P, lpeg.R, lpeg.S, lpeg.C, lpeg.Cs, lpeg.Ct, lpeg.Cc, lpeg.Cmt
44
45local variables    = interfaces and interfaces.variables
46local commands     = commands
47local context      = context
48local implement    = interfaces.implement
49
50local formatters   = string.formatters
51
52local v_default    = variables.default
53local v_small      = variables.small
54local v_medium     = variables.medium
55local v_big        = variables.big
56local v_normal     = variables.normal
57local v_fit        = variables.fit
58local v_on         = variables.on
59local v_none       = variables.none
60
61local topoints     = number.topoints
62local todimen      = string.todimen
63
64local trialtypesetting = context.trialtypesetting
65
66chemistry = chemistry or { }
67local chemistry = chemistry
68
69chemistry.instance   = "chemistry"
70chemistry.format     = "metafun"
71chemistry.method     = "double"
72
73local nofstructures  = 0
74
75local common_keys = {
76    b      = "line",
77    r      = "line",
78    sb     = "line",
79    sr     = "line",
80    rd     = "line",
81    rh     = "line",
82    rb     = "line",
83    rbd    = "line",
84    cc     = "line",
85    ccd    = "line",
86    line   = "line",
87    dash   = "line",
88    arrow  = "line",
89    c      = "fixed",
90    cd     = "fixed",
91    z      = "text",
92    zt     = "text",
93    zlt    = "text",
94    zrt    = "text",
95    rz     = "text",
96    rt     = "text",
97    lrt    = "text",
98    rrt    = "text",
99    label  = "text",
100    zln    = "number",
101    zrn    = "number",
102    rn     = "number",
103    lrn    = "number",
104    rrn    = "number",
105    zn     = "number",
106    number = "number",
107    mov    = "transform",
108    mark   = "transform",
109    move   = "transform",
110    diff   = "transform",
111    off    = "transform",
112    adj    = "transform",
113    sub    = "transform",
114}
115
116local front_keys = {
117    bb    = "line",
118    eb    = "line",
119    rr    = "line",
120    lr    = "line",
121    lsr   = "line",
122    rsr   = "line",
123    lrd   = "line",
124    rrd   = "line",
125    lrh   = "line",
126    rrh   = "line",
127    lrbd  = "line",
128    rrbd  = "line",
129    lrb   = "line",
130    rrb   = "line",
131    lrz   = "text",
132    rrz   = "text",
133    lsub  = "transform",
134    rsub  = "transform",
135}
136
137local one_keys = {
138    db    = "line",
139    tb    = "line",
140    bb    = "line",
141    dr    = "line",
142    hb    = "line",
143    bd    = "line",
144    bw    = "line",
145    oe    = "line",
146    sd    = "line",
147    rdb   = "line",
148    ldb   = "line",
149    ldd   = "line",
150    rdd   = "line",
151    ep    = "line",
152    es    = "line",
153    ed    = "line",
154    et    = "line",
155    au    = "line",
156    ad    = "line",
157    cz    = "text",
158    rot   = "transform",
159    dir   = "transform",
160    rm    = "transform",
161    mir   = "transform",
162}
163
164local ring_keys = {
165    db    = "line",
166    hb    = "line",
167    br    = "line",
168    lr    = "line",
169    rr    = "line",
170    lsr   = "line",
171    rsr   = "line",
172    lrd   = "line",
173    rrd   = "line",
174    lrb   = "line",
175    rrb   = "line",
176    lrh   = "line",
177    rrh   = "line",
178    lrbd  = "line",
179    rrbd  = "line",
180    dr    = "line",
181    eb    = "line",
182    er    = "line",
183    ed    = "line",
184    au    = "line",
185    ad    = "line",
186    s     = "line",
187    ss    = "line",
188    mid   = "line",
189    mids  = "line",
190    midz  = "text",
191    lrz   = "text",
192    rrz   = "text",
193    crz   = "text",
194    rot   = "transform",
195    mir   = "transform",
196    adj   = "transform",
197    lsub  = "transform",
198    rsub  = "transform",
199    rm    = "transform",
200}
201
202-- table.setmetatableindex(front_keys,common_keys)
203-- table.setmetatableindex(one_keys,common_keys)
204-- table.setmetatableindex(ring_keys,common_keys)
205
206-- or (faster but not needed here):
207
208front_keys = table.merged(front_keys,common_keys)
209one_keys   = table.merged(one_keys,common_keys)
210ring_keys  = table.merged(ring_keys,common_keys)
211
212local syntax = {
213    carbon         = { max = 4, keys = one_keys, },
214    alkyl          = { max = 4, keys = one_keys, },
215    newmanstagger  = { max = 6, keys = one_keys, },
216    newmaneclipsed = { max = 6, keys = one_keys, },
217    one            = { max = 8, keys = one_keys, },
218    three          = { max = 3, keys = ring_keys, },
219    four           = { max = 4, keys = ring_keys, },
220    five           = { max = 5, keys = ring_keys, },
221    six            = { max = 6, keys = ring_keys, },
222    seven          = { max = 7, keys = ring_keys, },
223    eight          = { max = 8, keys = ring_keys, },
224    nine           = { max = 9, keys = ring_keys, },
225    fivefront      = { max = 5, keys = front_keys, },
226    sixfront       = { max = 6, keys = front_keys, },
227    chair          = { max = 6, keys = front_keys, },
228    boat           = { max = 6, keys = front_keys, },
229    pb             = { direct = 'chem_pb;' },
230    pe             = { direct = 'chem_pe;' },
231    save           = { direct = 'chem_save;' },
232    restore        = { direct = 'chem_restore;' },
233    chem           = { direct = formatters['chem_symbol("\\chemicaltext{%s}");'], arguments = 1 },
234    space          = { direct = 'chem_symbol("\\chemicalsymbol[space]");' },
235    plus           = { direct = 'chem_symbol("\\chemicalsymbol[plus]");' },
236    minus          = { direct = 'chem_symbol("\\chemicalsymbol[minus]");' },
237    equals         = { direct = 'chem_symbol("\\chemicalsymbol[equals]");' },
238    gives          = { direct = formatters['chem_symbol("\\chemicalsymbol[gives]{%s}{%s}");'], arguments = 2 },
239    equilibrium    = { direct = formatters['chem_symbol("\\chemicalsymbol[equilibrium]{%s}{%s}");'], arguments = 2 },
240    mesomeric      = { direct = formatters['chem_symbol("\\chemicalsymbol[mesomeric]{%s}{%s}");'], arguments = 2 },
241    opencomplex    = { direct = 'chem_symbol("\\chemicalsymbol[opencomplex]");' },
242    closecomplex   = { direct = 'chem_symbol("\\chemicalsymbol[closecomplex]");' },
243    reset          = { direct = 'chem_reset;' },
244    mp             = { direct = formatters['%s'], arguments = 1 }, -- backdoor MP code - dangerous!
245}
246
247chemistry.definitions = chemistry.definitions or { }
248local definitions     = chemistry.definitions
249
250storage.register("chemistry/definitions",definitions,"chemistry.definitions")
251
252function chemistry.undefine(name)
253    definitions[lower(name)] = nil
254end
255
256function chemistry.define(name,spec,text)
257    name = lower(name)
258    local dn = definitions[name]
259    if not dn then
260        dn = { }
261        definitions[name] = dn
262    end
263    dn[#dn+1] = {
264        spec = settings_to_array_with_repeat(spec,true),
265        text = settings_to_array_with_repeat(text,true),
266    }
267end
268
269local metacode, variant, keys, max, txt, pstack, sstack, align
270local molecule = chemistry.molecule -- or use lpegmatch(chemistry.moleculeparser,...)
271
272local function fetch(txt)
273    local st = stack[txt]
274    local t = st.text[st.n]
275    while not t and txt > 1 do
276        txt = txt - 1
277        st = stack[txt]
278        t = st.text[st.n]
279    end
280    if t then
281        if trace_textstack then
282            report_chemistry("fetching from stack %a, slot %a, data %a",txt,st.n,t)
283        end
284        st.n = st.n + 1
285    end
286    return txt, t
287end
288
289local remapper = {
290    ["+"] = "p",
291    ["-"] = "m",
292    ["--"] = "mm",
293    ["++"] = "pp",
294}
295
296local dchrs     = R("09")
297local sign      = S("+-")
298local digit     = dchrs / tonumber
299local amount    = (sign^-1 * (dchrs^0 * P('.'))^-1 * dchrs^1) / tonumber
300local single    = digit
301local range     = digit * P("..") * digit
302local set       = Ct(digit^2)
303local colon     = P(":")
304local equal     = P("=")
305local other     = 1 - digit - colon - equal
306local remapped  = (sign * sign + sign) / remapper
307local operation = Cs(other^1)
308local special   = (colon * C(other^1)) + Cc("")
309local text      = (equal * C(P(1)^0)) + Cc(false)
310
311local pattern   =
312    (amount + Cc(1))
313  * (remapped + Cc(""))
314  * Cs(operation/lower)
315  * Cs(special/lower) * (
316        range * Cc(false) * text +
317        Cc(false) * Cc(false) * set * text +
318        single * Cc(false) * Cc(false) * text +
319        Cc(false) * Cc(false) * Cc(false) * text
320    )
321
322-- local n, operation, index, upto, set, text = lpegmatch(pattern,"RZ1357")
323
324-- print(lpegmatch(pattern,"RZ=x"))        -- 1 RZ false false false x
325-- print(lpegmatch(pattern,"RZ1=x"))       -- 1 RZ 1     false false x
326-- print(lpegmatch(pattern,"RZ1..3=x"))    -- 1 RZ 1     3     false x
327-- print(lpegmatch(pattern,"RZ13=x"))      -- 1 RZ false false table x
328
329local f_initialize       = 'if unknown context_chem : input mp-chem.mpiv ; fi ;'
330local f_start_structure  = formatters['chem_start_structure(%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%q);']
331local f_set_trace_bounds = formatters['chem_trace_boundingbox := %l ;']
332local f_stop_structure   = 'chem_stop_structure;'
333local f_start_component  = 'chem_start_component;'
334local f_stop_component   = 'chem_stop_component;'
335local f_line             = formatters['chem_%s%s(%s,%s,%s,%s,%q);']
336local f_set              = formatters['chem_set(%s);']
337local f_number           = formatters['chem_%s%s(%s,%s,"\\chemicaltext{%s}");']
338local f_text             = f_number
339local f_empty_normal     = formatters['chem_%s(%s,%s,"");']
340local f_empty_center     = formatters['chem_c%s(%s,%s,"");']
341local f_transform        = formatters['chem_%s(%s,%s,%s);']
342local f_fixed            = formatters['chem_%s(%s,%s,%q);']
343
344local function process(level,spec,text,n,rulethickness,rulecolor,offset,default_variant)
345    insert(stack,{ spec = spec, text = text, n = n })
346    local txt = #stack
347    local m = #metacode
348    local saved_rulethickness = rulethickness
349    local saved_rulecolor = rulecolor
350    local saved_align = align
351    local current_variant = default_variant or "six"
352    for i=1,#spec do
353        local step = spec[i]
354        local s = lower(step)
355        local n = current_variant .. ":" .. s
356        local d = definitions[n]
357        if not d then
358            n = s
359            d = definitions[n]
360        end
361        if d then
362            if trace_structure then
363                report_chemistry("level %a, step %a, definition %a, snippets %a",level,step,n,#d)
364            end
365            for i=1,#d do
366                local di = d[i]
367                current_variant = process(level+1,di.spec,di.text,1,rulethickness,rulecolor,offset,current_variant) -- offset?
368            end
369        else
370            local factor, osign, operation, special, index, upto, set, text = lpegmatch(pattern,step)
371            if trace_structure then
372                local set = set and concat(set," ") or "-"
373                report_chemistry("level %a, step %a, factor %a, osign %a, operation %a, special %a, index %a, upto %a, set %a, text %a",
374                    level,step,factor,osign,operation,special,index,upto,set,text)
375            end
376            if operation == "rulecolor" then
377                local t = text
378                if not t then
379                    txt, t = fetch(txt)
380                end
381                if t == v_default or t == v_normal or t == "" then
382                    rulecolor = saved_rulecolor
383                elseif t then
384                    rulecolor = t
385                end
386            elseif operation == "rulethickness" then
387                local t = text
388                if not t then
389                    txt, t = fetch(txt)
390                end
391                if t == v_default or t == v_normal or t == t_medium or t == "" then
392                    rulethickness = saved_rulethickness
393                elseif t == v_small then
394                    rulethickness = topoints(1/1.2 * todimen(saved_rulethickness))
395                elseif t == v_big then
396                    rulethickness = topoints(1.2 * todimen(saved_rulethickness))
397                elseif t then
398                 -- rulethickness = topoints(todimen(t)) -- mp can't handle sp
399                    rulethickness = topoints(tonumber(t) * todimen(saved_rulethickness))
400                end
401            elseif operation == "symalign" then
402                local t = text
403                if not t then
404                    txt, t = fetch(txt)
405                end
406                if t == v_default or t == v_normal then
407                    align = saved_align
408                elseif t and t ~= "" then
409                    align = "." .. t
410                end
411            elseif operation == "pb" then
412                insert(pstack,variant)
413                m = m + 1 ; metacode[m] = syntax.pb.direct
414                if keys[special] == "text" and index then
415                    if keys["c"..special] == "text" then -- can be option: auto ...
416                        m = m + 1 ; metacode[m] = f_empty_center(special,variant,index)
417                    else
418                        m = m + 1 ; metacode[m] = f_empty_normal(special,variant,index)
419                    end
420                end
421            elseif operation == "pe" then
422                variant = remove(pstack)
423                local ss = syntax[variant]
424                keys, max = ss.keys, ss.max
425                m = m + 1 ; metacode[m] = syntax.pe.direct
426                m = m + 1 ; metacode[m] = f_set(variant)
427                current_variant = variant
428            elseif operation == "save" then
429                insert(sstack,variant)
430                m = m + 1 ; metacode[m] = syntax.save.direct
431            elseif operation == "restore" then
432                if #sstack > 0 then
433                    variant = remove(sstack)
434                else
435                    report_chemistry("restore without save")
436                end
437                local ss = syntax[variant]
438                keys, max = ss.keys, ss.max
439                m = m + 1 ; metacode[m] = syntax.restore.direct
440                m = m + 1 ; metacode[m] = f_set(variant)
441                current_variant = variant
442            elseif operation then
443                local ss = syntax[operation]
444                local what = keys[operation]
445                local ns = 0
446                if set then
447                    local sv = syntax[current_variant]
448                    local ms = sv and sv.max
449                    set = unique(set)
450                    ns = #set
451                    if directive_strictorder then
452                        if what == "line" then
453                            set = sorted(set)
454                        end
455                        if directive_strictindex and ms then
456                            for i=ns,1,-1 do
457                                local si = set[i]
458                                if si > ms then
459                                    report_chemistry("level %a, operation %a, max nofsteps %a, ignoring %a",level,operation,ms,si)
460                                    set[i] = nil
461                                    ns = ns - 1
462                                else
463                                    break
464                                end
465                            end
466                        end
467                    else
468                        if directive_strictindex and ms then
469                            local t, nt = { }, 0
470                            for i=1,ns do
471                                local si = set[i]
472                                if si > ms then
473                                    report_chemistry("level %a, operation %a, max nofsteps %a, ignoring %a",level,operation,ms,si)
474                                    set[i] = nil
475                                else
476                                    nt = nt + 1
477                                    t[nt] = si
478                                end
479                            end
480                            ns = nt
481                            set = t
482                        end
483                    end
484                end
485                if ss then
486                    local ds = ss.direct
487                    if ds then
488                        local sa = ss.arguments
489                        if sa == 1 then
490                            local one ; txt, one = fetch(txt)
491                            m = m + 1 ; metacode[m] = ds(one or "")
492                        elseif sa == 2 then
493                            local one ; txt, one = fetch(txt)
494                            local two ; txt, two = fetch(txt)
495                            m = m + 1 ; metacode[m] = ds(one or "",two or "")
496                        else
497                            m = m + 1 ; metacode[m] = ds
498                        end
499                    elseif ss.keys then
500                        variant, keys, max = s, ss.keys, ss.max
501                        m = m + 1 ; metacode[m] = f_set(variant)
502                        current_variant = variant
503                    end
504                elseif what == "line" then
505                    local s = osign
506                    if s ~= "" then
507                        s = "." .. s
508                    end
509                    if set then
510                        -- condense consecutive numbers in a set to a range
511                        local sf, st = set[1]
512                        for i=1,ns do
513                            if i > 1 and set[i] ~= set[i-1]+1 then
514                                m = m + 1 ; metacode[m] = f_line(operation,s,variant,sf,st,rulethickness,rulecolor)
515                                sf = set[i]
516                            end
517                            st = set[i]
518                        end
519                        m = m + 1 ; metacode[m] = f_line(operation,s,variant,sf,st,rulethickness,rulecolor)
520                    elseif upto then
521                        m = m + 1 ; metacode[m] = f_line(operation,s,variant,index,upto,rulethickness,rulecolor)
522                    elseif index then
523                        m = m + 1 ; metacode[m] = f_line(operation,s,variant,index,index,rulethickness,rulecolor)
524                    else
525                        m = m + 1 ; metacode[m] = f_line(operation,s,variant,1,max,rulethickness,rulecolor)
526                    end
527                elseif what == "number" then
528                    if set then
529                        for i=1,ns do
530                            local si = set[i]
531                            m = m + 1 ; metacode[m] = f_number(operation,align,variant,si,si)
532                        end
533                    elseif upto then
534                        for i=index,upto do
535                            local si = set[i]
536                            m = m + 1 ; metacode[m] = f_number(operation,align,variant,si,si)
537                        end
538                    elseif index then
539                        m = m + 1 ; metacode[m] = f_number(operation,align,variant,index,index)
540                    else
541                        for i=1,max do
542                            m = m + 1 ; metacode[m] = f_number(operation,align,variant,i,i)
543                        end
544                    end
545                elseif what == "text" then
546                    if set then
547                        for i=1,ns do
548                            local si = set[i]
549                            local t = text
550                            if not t then txt, t = fetch(txt) end
551                            if t then
552                                t = molecule(processor_tostring(t))
553-- local p, t = processors.split(t)
554-- m = m + 1 ; metacode[m] = f_text(operation,p or align,variant,si,t)
555                                m = m + 1 ; metacode[m] = f_text(operation,align,variant,si,t)
556                            end
557                        end
558                    elseif upto then
559                        for i=index,upto do
560                            local t = text
561                            if not t then txt, t = fetch(txt) end
562                            if t then
563                                t = molecule(processor_tostring(t))
564                                m = m + 1 ; metacode[m] = f_text(operation,align,variant,i,t)
565                            end
566                        end
567                    elseif index == 0 then
568                        local t = text
569                        if not t then txt, t = fetch(txt) end
570                        if t then
571                            t = molecule(processor_tostring(t))
572                            m = m + 1 ; metacode[m] = f_text(operation,align,variant,index,t)
573                        end
574                    elseif index then
575                        local t = text
576                        if not t then txt, t = fetch(txt) end
577                        if t then
578                            t = molecule(processor_tostring(t))
579                            m = m + 1 ; metacode[m] = f_text(operation,align,variant,index,t)
580                        end
581                    else
582                        for i=1,max do
583                            local t = text
584                            if not t then txt, t = fetch(txt) end
585                            if t then
586                                t = molecule(processor_tostring(t))
587                                m = m + 1 ; metacode[m] = f_text(operation,align,variant,i,t)
588                            end
589                        end
590                    end
591                elseif what == "transform" then
592                    if osign == "m" then
593                        factor = -factor
594                    end
595                    if set then
596                        for i=1,ns do
597                            local si = set[i]
598                            m = m + 1 ; metacode[m] = f_transform(operation,variant,si,factor)
599                        end
600                    elseif upto then
601                        for i=index,upto do
602                            m = m + 1 ; metacode[m] = f_transform(operation,variant,i,factor)
603                        end
604                    else
605                        m = m + 1 ; metacode[m] = f_transform(operation,variant,index or 1,factor)
606                    end
607                elseif what == "fixed" then
608                    m = m + 1 ; metacode[m] = f_fixed(operation,variant,rulethickness,rulecolor)
609                elseif trace_structure then
610                    report_chemistry("level %a, ignoring undefined operation %s",level,operation)
611                end
612            end
613        end
614    end
615    remove(stack)
616    return current_variant
617end
618
619-- the size related values are somewhat special but we want to be
620-- compatible
621--
622-- rulethickness in points
623
624local function checked(d,bondlength,unit,scale)
625    if d == v_none then
626        return 0
627    end
628    local n = tonumber(d)
629    if not n then
630        -- assume dimen
631    elseif n >= 10 or n <= -10 then
632        return bondlength * unit * n / 1000
633    else
634        return bondlength * unit * n
635    end
636    local n = todimen(d)
637    if n then
638        return scale * n
639    else
640        return v_fit
641    end
642end
643
644local function calculated(height,bottom,top,bondlength,unit,scale)
645    local scaled = 0
646    if height == v_none then
647        -- this always wins
648        height = "0pt"
649        bottom = "0pt"
650        top    = "0pt"
651    elseif height == v_fit then
652        height = "true"
653        bottom = bottom == v_fit and "true" or topoints(checked(bottom,bondlength,unit,scale))
654        top    = top    == v_fit and "true" or topoints(checked(top,   bondlength,unit,scale))
655    else
656        height = checked(height,bondlength,unit,scale)
657        if bottom == v_fit then
658            if top == v_fit then
659                bottom  = height / 2
660                top     = bottom
661            else
662                top     = checked(top,bondlength,unit,scale)
663                bottom  = height - top
664            end
665        elseif top == v_fit then
666            bottom = checked(bottom,bondlength,unit,scale)
667            top    = height - bottom
668        else
669            bottom  = checked(bottom,bondlength,unit,scale)
670            top     = checked(top,   bondlength,unit,scale)
671            local ratio = height / (bottom+top)
672            bottom  = bottom  * ratio
673            top     = top     * ratio
674        end
675        scaled = height
676        top    = topoints(top)
677        bottom = topoints(bottom)
678        height = topoints(height)
679    end
680    return height, bottom, top, scaled
681end
682
683function chemistry.start(settings)
684    --
685    local width          = settings.width         or v_fit
686    local height         = settings.height        or v_fit
687    local unit           = settings.unit          or 655360
688    local bondlength     = settings.factor        or 3
689    local rulethickness  = settings.rulethickness or 65536
690    local rulecolor      = settings.rulecolor     or "black"
691    local axiscolor      = settings.framecolor    or "black"
692    local scale          = settings.scale         or "normal"
693    local rotation       = settings.rotation      or 0
694    local offset         = settings.offset        or 0
695    local left           = settings.left          or v_fit
696    local right          = settings.right         or v_fit
697    local top            = settings.top           or v_fit
698    local bottom         = settings.bottom        or v_fit
699    --
700    align = settings.symalign or "auto"
701    if trace_structure then
702        report_chemistry("unit %p, bondlength %s, symalign %s",unit,bondlength,align)
703    end
704    if align ~= "" then
705        align = "." .. align
706    end
707    if trace_structure then
708        report_chemistry("%s scale %a, rotation %a, width %s, height %s, left %s, right %s, top %s, bottom %s","asked",scale,rotation,width,height,left,right,top,bottom)
709    end
710    if scale == v_small then
711        scale = 1/1.2
712    elseif scale == v_normal or scale == v_medium or scale == 0 then
713        scale = 1
714    elseif scale == v_big then
715        scale = 1.2
716    else
717        scale = tonumber(scale)
718        if not scale or scale == 0 then
719            scale = 1
720        elseif scale >= 10 then
721            scale = scale / 1000
722        elseif scale < .01 then
723            scale = .01
724        end
725    end
726    --
727    unit = scale * unit
728    --
729    local sp_width  = 0
730    local sp_height = 0
731    --
732    width,  left,   right, sp_width  = calculated(width, left,  right,bondlength,unit,scale)
733    height, bottom, top,   sp_height = calculated(height,bottom,top,  bondlength,unit,scale)
734    --
735    if width ~= "true" and height ~= "true" and trialtypesetting() then
736        if trace_structure then
737            report_chemistry("skipping trial run")
738        end
739        context.rule(sp_width,sp_height,0) -- maybe depth
740        return
741    end
742    --
743    nofstructures = nofstructures + 1
744    --
745    rotation = tonumber(rotation) or 0
746    --
747    metacode = { }
748    --
749    if trace_structure then
750        report_chemistry("%s scale %a, rotation %a, width %s, height %s, left %s, right %s, top %s, bottom %s","used",scale,rotation,width,height,left,right,top,bottom)
751    end
752    metacode[#metacode+1] = f_start_structure(
753        nofstructures,
754        left, right, top, bottom,
755        rotation, topoints(unit), bondlength, scale, topoints(offset),
756        tostring(settings.axis == v_on), topoints(rulethickness), axiscolor
757    )
758    metacode[#metacode+1] = f_set_trace_bounds(trace_boundingbox) ;
759    --
760    variant, keys, stack, pstack, sstack = "one", { }, { }, { }, { }
761end
762
763function chemistry.stop()
764    if metacode then
765        metacode[#metacode+1] = f_stop_structure
766        local mpcode = concat(metacode,"\n")
767        if trace_metapost then
768            report_chemistry("metapost code:\n%s", mpcode)
769        end
770        metapost.graphic {
771            instance    = chemistry.instance,
772            format      = chemistry.format,
773            method      = chemistry.method,
774            data        = mpcode,
775            definitions = f_initialize,
776        }
777        metacode = nil
778    end
779end
780
781function chemistry.component(spec,text,rulethickness,rulecolor)
782    if metacode then
783        local spec = settings_to_array_with_repeat(spec,true) -- no lower?
784        local text = settings_to_array_with_repeat(text,true)
785        metacode[#metacode+1] = f_start_component
786        process(1,spec,text,1,rulethickness,rulecolor)
787        metacode[#metacode+1] = f_stop_component
788    end
789end
790
791statistics.register("chemical formulas", function()
792    if nofstructures > 0 then
793        return format("%s chemical structure formulas",nofstructures) -- no timing needed, part of metapost
794    end
795end)
796
797-- interfaces
798
799implement {
800    name      = "undefinechemical",
801    actions   = chemistry.undefine,
802    arguments = "string"
803}
804
805implement {
806    name      = "definechemical",
807    actions   = chemistry.define,
808    arguments = "3 strings",
809}
810
811implement {
812    name      = "startchemical",
813    actions   = chemistry.start,
814    arguments = {
815        {
816            { "width" },
817            { "height" },
818            { "left" },
819            { "right" },
820            { "top" },
821            { "bottom" },
822            { "scale" },
823            { "rotation" },
824            { "symalign" },
825            { "axis" },
826            { "framecolor" },
827            { "rulethickness", "dimension" },
828            { "offset", "dimension" },
829            { "unit", "dimension" },
830            { "factor", "integer" }
831        }
832    }
833}
834
835implement {
836    name      = "stopchemical",
837    actions   = chemistry.stop,
838}
839
840implement {
841    name      = "chemicalcomponent",
842    actions   = chemistry.component,
843    arguments = "4 strings",
844}
845
846-- todo: top / bottom
847-- note that "<->" here differs from ppchtex
848
849local inline = {
850    ["single"]      = "\\chemicalsinglebond",  ["-"]    = "\\chemicalsinglebond",
851 --                                            ["−"]    = "\\chemicalsinglebond",
852    ["double"]      = "\\chemicaldoublebond",  ["--"]   = "\\chemicaldoublebond",
853                                               ["="]    = "\\chemicaldoublebond",
854    ["triple"]      = "\\chemicaltriplebond",  ["---"]  = "\\chemicaltriplebond",
855                                               [""]    = "\\chemicaltriplebond",
856    ["gives"]       = "\\chemicalgives",       ["->"]   = "\\chemicalgives",
857    ["equilibrium"] = "\\chemicalequilibrium", ["<-->"] = "\\chemicalequilibrium",
858                                               ["<=>"]  = "\\chemicalequilibrium",
859    ["mesomeric"]   = "\\chemicalmesomeric",   ["<>"]   = "\\chemicalmesomeric",
860                                               ["<->"]  = "\\chemicalmesomeric",
861    ["plus"]        = "\\chemicalplus",        ["+"]    = "\\chemicalplus",
862    ["minus"]       = "\\chemicalminus",
863    ["equals"]      = "\\chemicalequals",
864    ["space"]       = "\\chemicalspace",
865}
866
867local ctx_chemicalinline  = context.chemicalinline
868local ctx_chemicalspecial = context.chemicalspecial
869
870function chemistry.inlinechemical(spec)
871    local spec = settings_to_array_with_repeat(spec,true)
872    for i=1,#spec do
873        local s = spec[i]
874        local inl = inline[lower(s)]
875        if inl then
876            ctx_chemicalspecial(inl)
877        else
878            ctx_chemicalinline(molecule(s))
879        end
880    end
881end
882
883implement {
884    name      = "inlinechemical",
885    actions   = chemistry.inlinechemical,
886    arguments = "string"
887}
888
889-- This is the new approach:
890
891do
892
893    local short_mapping = {
894        ["-"]    = "\\csinglebond ",
895     -- ["−"]    = "\\csinglebond ",
896        ["--"]   = "\\cdoublebond ",
897        ["="]    = "\\cdoublebond ",
898        ["---"]  = "\\ctriplebond ",
899        [""]    = "\\ctriplebond ",
900        ["->"]   = "\\cgives ",
901        ["<-"]   = "\\creturns ",
902        ["<->"]  = "\\cmesomeric ",
903        ["-->"]  = "\\clonggives ",
904        ["<--"]  = "\\clongreturns ",
905        ["<-->"] = "\\clongmesomeric ",
906        ["<=>"]  = "\\cequilibrium ",
907        ["<="]   = "\\cleaningleft ",
908        ["=>"]   = "\\cleaningright ",
909        ["<==>"] = "\\clongequilibrium ",
910        ["<=="]  = "\\clongleaningleft ",
911        ["==>"]  = "\\clongleaningright ",
912    }
913
914    local long_mapping = {
915        ["single"]        = "\\csinglebond ",
916        ["double"]        = "\\cdoublebond ",
917        ["triple"]        = "\\ctriplebond ",
918        ["gives"]         = "\\cgives ",        ["lgives"]        = "\\clonggives ",
919        ["returns"]       = "\\creturns ",      ["lreturns"]      = "\\clongreturns ",
920        ["mesomeric"]     = "\\cmesomeric ",    ["lmesomeric"]    = "\\clongmesomeric ",
921        ["equilibrium"]   = "\\cequilibrium ",  ["lequilibrium"]  = "\\clongequilibrium ",
922        ["leaningleft"]   = "\\cleaningleft ",  ["lleaningleft"]  = "\\clongleaningleft ",
923        ["leaningright"]  = "\\cleaningright ", ["lleaningright"] = "\\clongleaningright ",
924     -- ["plus"]          = "\\cplus ",
925     -- ["minus"]         = "\\cminus ",
926     -- ["equals"]        = "\\cequals ",
927    }
928
929    local sign      = S("+-")
930    local script    = S("^_")
931    local character = lpegpatterns.utf8character
932    local cardinal  = lpegpatterns.cardinal
933    local spaces    = lpegpatterns.whitespace
934    local nospaces  = spaces^0/""
935
936    local short     = lpeg.utfchartabletopattern(short_mapping)
937    local long      = lpeg.utfchartabletopattern(long_mapping)
938
939    local csname    = P("\\") * R("az")^1 -- \left[ * lpegpatterns.nestedbrackets^-1
940
941    local pattern = Cs ((
942        csname
943      + spaces * (short / short_mapping) * spaces
944      + spaces * (long  / long_mapping) * spaces
945      + script^1 * nospaces * Cc("{") * nospaces * (sign^-1 * cardinal^1 + sign) * Cc("}")
946      + character
947    )^0)
948
949    interfaces.implement {
950        name      = "ic",
951        arguments = "string",
952        actions   = function(s)
953            context(lpegmatch(pattern,s))
954        end
955    }
956
957end
958