if not modules then modules = { } end modules ['node-ali'] = { version = 1.001, comment = "companion to node-ini.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } local aligncharacter_code = 1 local mathalign_code = 2 local mathmatrix_code = 3 local setmetatableindex = table.setmetatableindex local nuts = nodes.nuts local tonut = nuts.tonut local tonode = nuts.tonode local getwidth = nuts.getwidth local setwidth = nuts.setwidth local getid = nuts.getid local getattr = nuts.getattr local setnext = nuts.setnext local getnext = nuts.getnext local getprev = nuts.getprev local getboth = nuts.getboth local getglue = nuts.getglue local setglue = nuts.setglue local getwhd = nuts.getwhd local setwhd = nuts.setwhd local setpenalty = nuts.setpenalty local getlist = nuts.getlist local setlist = nuts.setlist local getleader = nuts.getleader local setattrlist = nuts.setattrlist local setprop = nuts.setprop local getprop = nuts.getprop local getfont = nuts.getfont local getchar = nuts.getchar local addmargins = nuts.addmargins local findtail = nuts.tail local hasglyph = nuts.hasglyph local getwordrange = nuts.getwordrange local dimensions = nuts.rangedimensions local flushnode = nuts.flush local hpack = nuts.hpack local repack = nuts.repack local insertbefore = nuts.insertbefore local insertafter = nuts.insertafter local effectiveglue = nuts.effectiveglue local newkern = nuts.pool.kern local newrule = nuts.pool.rule local newglue = nuts.pool.glue local traversers = nuts.traversers local nextrecord = traversers.alignrecord local nextunset = traversers.unset local nextglyph = traversers.glyph local nextglue = traversers.glue local nextpenalty = traversers.penalty local nextboundary = traversers.boundary local nextnode = traversers.node local nextlist = traversers.list local nextrule = traversers.rule local nodecodes = nodes.nodecodes local glyph_code = nodecodes.glyph local glue_code = nodecodes.glue local kern_code = nodecodes.kern local disc_code = nodecodes.disc local unset_code = nodecodes.unset local alignrecord_code = nodecodes.alignrecord local rule_code = nodecodes.rule local temp_code = nodecodes.temp local spaceskip_code = nodes.gluecodes.spaceskip local xspaceskip_code = nodes.gluecodes.xspaceskip local intermathskip_code = nodes.gluecodes.intermathskip local fontkern_code = nodes.kerncodes.fontkern local row_code = nodes.listcodes.alignment -- should be row local cell_code = nodes.listcodes.cell local line_code = nodes.listcodes.line local linepenalty_code = nodes.penaltycodes.linepanalty -- local preamble_pass = tex.alignmentcontextcodes.preamble -- local preroll_pass = tex.alignmentcontextcodes.preroll -- local wrapup_pass = tex.alignmentcontextcodes.wrapup -- todo statistics and tracing do local a_alignchar = attributes.private("aligncharacter") local method = 2 local unislots = fonts.hashes.unislots -- todo local chardata = fonts.hashes.characters function nodes.handlers.aligncharacter(head,where,callback,attr,preamble) -- 0x1 if callback ~= aligncharacter_code then -- nothing to do here elseif where == "preroll" then local attr = getattr(attr,a_alignchar) -- 1 : value doesn't matter (for now) if attr then local widths = { } local data = { } local rows = 0 local cols = 0 for col in nextrecord, preamble do cols = cols + 1 local w, s = getwidth(col,true) widths[cols] = { col, w, s } end -- for row in nextunset, head do rows = rows + 1 local c = 0 local d = { } data[rows] = d for col in nextunset, getlist(row) do c = c + 1 if widths[c][2] then local list = getlist(col) -- if method == 1 then -- local left = nil -- local right = nil -- local middle = nil -- for g, char in nextglyph, list do -- if not left then -- left = g -- end -- if char == getattr(g,a_alignchar) then -- middle = g -- end -- right = g -- end -- d[c] = middle and { col, left, middle, right, 0, 0, getwidth(middle) } or false -- elseif method == 2 then local middle = nil -- we can either cache unislots or we can cache for this font for g, char, font in nextglyph, list do local unicode = getattr(g,a_alignchar) if unicode then if char == unicode then middle = g elseif unislots[font][char] == unicode then middle = g end end end if middle then local left, right = getwordrange(middle) -- not real gain but handy anyway (less code too) -- local left = middle -- local right = middle -- for g, id, subtype in nextnode, middle do -- if id == glyph_code or id == disc_code then -- right = g -- elseif id == kern_code and subtype == fontkern_code then -- right = g -- else -- break -- end -- end -- for g, id, subtype in prevnode, middle do -- if id == glyph_code or id == disc_code then -- left = g -- elseif id == kern_code and subtype == fontkern_code then -- left = g -- else -- break -- end -- end d[c] = { col, left, middle, right, 0, 0, getwidth(middle) } else d[c] = false end -- else -- local middle = nil -- for g, char in nextglyph, list do -- if char == getattr(g,a_alignchar) then -- middle = g -- end -- end -- if middle then -- local left = list -- local right = findtail(list) -- if getid(left) == glue_code then -- left = getnext(left) -- end -- if getid(right) == glue_code then -- right = getprev(right) -- end -- d[c] = { col, left, middle, right, 0, 0, getwidth(middle) } -- else -- d[c] = false -- end -- end else d[c] = false end end end -- for col=1,cols do local maxl = 0 local maxr = 0 local minm = 0 local maxm = 0 local colw = widths[col] for row=1,rows do local d = data[row][col] if d then local p = d[1] local l = d[2] local m = d[3] local r = d[4] if m then local lw = l == m and 0 or dimensions(p,l,m) local rw = m == r and 0 or dimensions(p,getnext(m),getnext(r)) d[5] = lw d[6] = rw if lw > maxl then maxl = lw end if rw > maxr then maxr = rw end local mw = d[7] if maxm == 0 then minm = mw maxm = mw else if mw > maxm then maxm = mw end if mw < minm then minm = mw end end end end end -- local fixedwidth = colw[3] ~= 0 -- local old = colw[2] local new = old for row=1,rows do local d = data[row][col] if d then local p = d[1] local l = d[2] local m = d[3] local r = d[4] if l and m and r then local lw = d[5] local rw = d[6] local mw = d[7] dl = maxl - lw dr = maxr - rw if dl ~= 0 or dr ~= 0 or mw ~= maxm then local lst = getlist(p) local wid = getwidth(p) if dl ~= 0 then local k = newkern(dl) lst = insertbefore(lst,l,k) setattrlist(k,m) setlist(p,lst) wid = wid + dl end if dr ~= 0 then local k = newkern(dr) insertafter(lst,r,k) setattrlist(k,m) wid = wid + dr end if mw ~= maxm then local dw = (maxm - mw) local dx = dw / 2 addmargins(m,-dx,-dx) wid = wid + dw end setwidth(p,wid) if wid > new then new = wid end setlist(p,lst) -- somewhat fuzzy: if fixedwidth then local l = hpack(h,getwidth(p),"exactly") setglue(p,getglue(l)) setlist(l) flushnode(l) else setglue(p) end -- end end end end if new > old then if fixedwidth then -- issue overflow warning else setwidth(colw[1],new) end end end end end end local enabled = false interfaces.implement { name = "enablealignmentcharacter", -- onlyonce = true, public = true, protected = true, actions = function() if not enabled then nodes.tasks.enableaction("alignments", "nodes.handlers.aligncharacter") -- 0x1 enabled = true end end, } end -- This will go to math-ali.lmt do local getdata = nuts.getdata local removenode = nuts.remove local getwhd = nuts.getwhd local getheight = nuts.getheight local getdepth = nuts.getdepth local setheight = nuts.setheight local setdepth = nuts.setdepth local getglue = nuts.getglue local setoffsets = nuts.setoffsets local setsubtype = nuts.setsubtype local baselineskip_code = nodes.gluecodes.baselineskip local lineskip_code = nodes.gluecodes.lineskip local alignrecord_code = nodecodes.alignrecord local hlist_code = nodecodes.hlist local unset_code = nodecodes.unset -- local rule_code = nodecodes.rule local nextnode = nuts.traversers.node local texgetdimen = tex.getdimen local texsetdimen = tex.setdimen local texgetglue = tex.getglue local texget = tex.get local leftmarker = tex.boundaries.system("c_math_align_l_marker") local rightmarker = tex.boundaries.system("c_math_align_r_marker") local a_location = attributes.system("mathnumberlocation") local a_threshold = attributes.system("mathnumberthreshold") local v_first = interfaces.variables.first local v_last = interfaces.variables.last local v_both = interfaces.variables.both -- Here: local function openup(specification,head) local inbetween = specification.inbetween or 0 local height = specification.height or 0 local depth = specification.depth or 0 local splitmethod = specification.splitmethod or "" local lines = { } for n, id, subtype, list in nextlist, head do lines[#lines+1] = { n, subtype, getwhd(n) } end local noflines = #lines if noflines > 0 then local currentline = 1 for n, subtype in nextglue, head do -- print(currentline,height/65536,depth/65536) -- one day we can decide what to do with intertext stuff based on the -- subtype but not now ... on our agenda (intertext etc) if subtype == baselineskip_code or subtype == lineskip_code then local nextline = currentline + 1 local amount, stretch, shrink = getglue(n) -- todo: safeguard for no nextline local prevdp = lines[currentline][5] local nextht = lines[nextline][4] -- todo: add catch for no line here ^^^ local delta = 0 if prevdp < depth then setdepth(lines[currentline][1],depth) delta = delta + (depth - prevdp) -- lines[currentline][5] = depth end if nextht < height then setheight(lines[nextline][1],height) -- lines[nextline][4] = height delta = delta + (height - nextht) end -- if subtype == lineskip_code then -- setglue(n,inbetween,stretch,shrink) setsubtype(n,baselineskip_code) -- else setglue(n,amount+inbetween-delta,stretch,shrink) -- end currentline = nextline -- was curline -- if currentline > noflines then -- break -- end end end if splitmethod ~= "" then local currentline = 0 for n, subtype in nextpenalty, head do if subtype == linepenalty_code then if l == 1 then if splitmethod == v_both or splitmethod == v_first then -- print("FIRST") setpenalty(n, 10000) end elseif l == noflines then if splitmethod == v_both or splitmethod == v_last then -- print("LAST") setpenalty(n, 10000) end end curline = currentline end end end local firstht = lines[1][4] local lastdp = lines[noflines][5] if firstht < height then setheight(lines[1],height) end if lastdp < depth then setdepth(lines[noflines],depth) end end end nuts.openup = openup -- When present, the number is after the right marker. We need to move the -- number when we want it at the left. -- -- Todo: set a flag in mathalignment and support atttibutes on it so that -- we can check if this is needed. -- [dummy] [left -2] [second -1] [number 0] -- In the end it makes more sense to just calculate the alignment in lua -- but it is kind of fun to see how we can control alignments. This code -- is (in the end) too complex. local totals = { } local widths = { } local records = { } local deltas = { } local cellwidths = { } local a_flushleft = 1 local a_centered = 2 local a_flushright = 3 local a_numberright = 1 local a_numberleft = 2 local function first_pass(head,callback,attr,preamble,signal) -- local width = 0 local hsize = texget("hsize") local count = 0 local overflow = false totals = { } -- maybe use one table widths = { } records = { } deltas = { } for n in nextrecord, preamble do local wd = getwidth(n) count = count + 1 width = width + wd totals [count] = width widths [count] = wd records[count] = n deltas [count] = 0 end -- local lindex = 0 local rindex = 0 local lwidth = 0 local rwidth = 0 local centered = false -- for row in nextunset, head do local count = 0 local anchor = nil local rochan = nil -- local cellwidths = { } for cell in nextunset, getlist(row) do local list = getlist(cell) count = count + 1 cellwidths[count] = getwidth(cell) for bound in nextboundary, list do local marker = getdata(bound) if marker == leftmarker then lindex = count anchor = bound rochan = list elseif marker == rightmarker then local n = getnext(bound) if n and getid(n) == hlist_code then local wd, ht, dp = getwhd(n) local lc = getattr(n,a_location) if lc then -- todo: just store align in the outer attribute so once ... -- local align = lc // 0x10 -- local location = lc % 0x10 local packed = (lc >> 8) & 0xF local align = (lc >> 4) & 0xF local location = (lc ) & 0xF local threshold = getattr(n,a_threshold) if location == a_numberright then -- (more advanced than left) local m = 1 local s = align == a_centered and 2 or 1 if align == a_flushleft then rwidth = wd elseif align == a_flushright then rwidth = wd elseif wd > lwidth then lwidth = wd rwidth = wd centered = true end if packed > 0 then setprop(n,"mathalignshift","right") lwidth = 0 rwidth = 0 end if totals[count-2] + cellwidths[count-1] + s*wd - s*threshold > hsize then local total = ht + dp setdepth(row,getdepth(row) + total) setoffsets(n,0,-total) local pr = records[count-1] local cw = getwidth(pr) if cw - wd > deltas[count-1] then deltas[count-1] = cw - wd end overflow = true end elseif location == a_numberleft then if align == a_flushleft then lwidth = wd elseif align == a_flushright then lwidth = wd end if signal == 0x80 then lwidth = wd end if packed > 0 then setprop(n,"mathalignshift","left") lwidth = 0 rwidth = 0 end end if location == a_numberleft and anchor then local l, l, n = removenode(list,n) if l ~= list then setlist(cell,l) -- setwidth(cell,0) end if signal == 0x80 then setwidth(cell,lwidth) end insertafter(rochan,anchor,n) end end end rindex = count end end end end -- if overflow then if deltas[rindex-1] ~= 0 then setwidth(records[rindex-1],deltas[rindex-1]) end end -- for count=1,#records do if count == lindex then if centered and overflow then lwidth = lwidth - texgetdimen("d_math_eqalign_number_distance") end setwidth(records[count],lwidth) elseif count == rindex then setwidth(records[count],rwidth) end end end local function second_pass(head,callback,attr,preamble,signal) local done = setmetatableindex("table") local glues = { } local okay = false for row, id, subtype in nextlist, head do if id == hlist_code and subtype == row_code then for cell, id, subtype in nextlist, getlist(row) do if id == hlist_code and subtype == cell_code then for n, s in nextglue, getlist(cell) do if s == intermathskip_code then local e = effectiveglue(n,cell) local g = getglue(n) local f = getfont(n) local a = done[f] local d = a[g] glues[n] = g if not d then a[g] = e elseif d > e then a[g] = e okay = true end end end end end end end if okay then for k, v in next, glues do local g = done[getfont(k)][v] if g then setglue(k,g) setprop(k,"fixedmathalign",true) end end for row, id, subtype in nextlist, head do if id == hlist_code and subtype == row_code then for cell, id, subtype, list in nextlist, getlist(row) do if list and id == hlist_code and subtype == cell_code then local wd = getwidth(cell) repack(cell,wd,"exactly") end end end end end end local function third_pass(head,callback,attr,preamble,signal) local inbetween, stretch, shrink = texgetglue("s_strc_math_alignment_inbetween") openup({ inbetween = inbetween }, head) end -- This will become a bit more pluggable so that we have less checking. -- maybe zero pass: preamble pass function nodes.handlers.mathalignrelocate(head,where,callback,attr,preamble) -- 0x2 if callback ~= mathalign_code then -- nothing to do here elseif where == "preroll" then local signal = getattr(attr,a_location) if signal == 0x20 or signal == 0x40 or signal == 0x80 then first_pass(head,callback,attr,preamble,signal) end elseif where == "wrapup" then local signal = getattr(attr,a_location) if signal == 0x40 or signal == 0x80 then second_pass(head,callback,attr,preamble,signal) end if signal then -- also 0x00 third_pass(head,callback,attr,preamble,signal) end end end do local a_mathalignmentvrule = attributes.private("mathalignmentvrule") local a_mathalignmenthrule = attributes.private("mathalignmenthrule") local a_mathalignmenttop = attributes.private("mathalignmenttop") local a_mathalignmentmid = attributes.private("mathalignmentmid") local a_mathalignmentbot = attributes.private("mathalignmentbot") local function hrule_pass(head,attr,preamble) local i = 0 for row, id, subtype, list in nextlist, head do if id == hlist_code and subtype == row_code then local height = 0 local depth = 0 local done = false for cell, id, subtype, list in nextlist, list do if list then -- look for leaders for n, id, subtype in nextglue, list do -- here we can have the attribute on the glue (leader) -- but more consistent is to have it on the rule local rule = getleader(n) if rule then local signal = getattr(rule,a_mathalignmenthrule) if signal then local w, h, d = getwhd(n) if h and d then if h > height then height = h end if d > depth then depth = d end done = true end end end end end end if done then setheight(row,height) setdepth(row,depth) end end end end -- todo : cache -- maybe : change prev pointer of head (is now temp) local function vrule_pass(head,attr,preamble) for row, id, subtype, list in nextlist, head do if id == hlist_code and subtype == row_code then local prv, nxt = getboth(row) for cell, id, subtype, list in nextlist, list do if list then -- look for vertical rules local d = false local h = false for n, id, subtype in nextrule, list do local signal = getattr(n,a_mathalignmentvrule) -- always 1 if signal then if not d then if prv then if getattr(prv,a_mathalignmentvrule) == 4 then local p = prv while p do local a = getattr(p,a_mathalignmentvrule) if not a then break elseif a == 2 then if not getprev(p) or getid(getprev(p)) == temp_code then local h = getattr(p,a_mathalignmenttop) or 0 nuts.setheight(p,h) end break else p = getprev(p) end end else prv = nil end -- if getid(prv) ~= rule_code or not getattr(prv,a_mathalignmentvrule) then -- prv = nil -- end end if nxt then if getattr(nxt,a_mathalignmentvrule) == 2 then local p = nxt while p do local a = getattr(p,a_mathalignmentvrule) if not a then break elseif a == 4 then if not getnext(p) then local d = getattr(p,a_mathalignmentbot) or 0 nuts.setdepth(p,d) end break else p = getnext(p) end end else nxt = nil end -- if getid(nxt) ~= rule_code or not getattr(nxt,a_mathalignmentvrule) then -- nxt = nil -- end end d = prv and -getdepth (prv) or 0 h = nxt and -getheight(nxt) or 0 d = d - (getattr(prv,a_mathalignmentmid) or 0) end setoffsets(n,nil,nil,d,h) end end end end end end end function nodes.handlers.mathmatrixrules(head,where,callback,attr,preamble) -- 0x3 if callback ~= mathmatrix_code then -- nothing to do here elseif where == "wrapup" then hrule_pass(head,attr,preamble) vrule_pass(head,attr,preamble) end end local enabled = false interfaces.implement { name = "enablematrixrules", -- onlyonce = true, public = true, protected = true, actions = function() if not enabled then nodes.tasks.enableaction("alignments","nodes.handlers.mathmatrixrules") -- 0x3 enabled = true end end, } end -- local a_ornament = attributes.system("mathmatrixornament") local leftornament = tex.boundaries.system("c_math_matrix_ornament_l") local rightornament = tex.boundaries.system("c_math_matrix_ornament_r") local topornament = tex.boundaries.system("c_math_matrix_ornament_t") local bottomornament = tex.boundaries.system("c_math_matrix_ornament_b") local verticalline = tex.boundaries.system("c_math_matrix_vl_boundary") local left = 0 local right = 0 local nofcells = 0 local found = false local lefts = false local rights = false local tops = false local bottoms = false local function first_pass(head,callback,attr,preamble,signal) nofcells = 0 left = 0 right = 0 found = false lefts = { } rights = { } tops = { } bottoms = { } for n in nextrecord, preamble do nofcells = nofcells + 1 end local cells = { } for row in nextunset, head do local c = 0 for cell in nextunset, getlist(row) do local list = getlist(cell) c = c + 1 for bound in nextboundary, list do local ornament = getdata(bound) if ornament == verticalline then -- this hack is is beyond ugly as we omit and span c = c + 1 break elseif ornament == leftornament then if c == 1 then local w = getwidth(cell) if w > left then left = w end setwidth(cell,0) cells[c+1] = true cells[c] = true found = true lefts[#lefts+1] = cell end elseif ornament == rightornament then if c == nofcells then local w = getwidth(cell) if w > right then right = w end setwidth(cell,0) cells[c-1] = true cells[c] = true found = true rights[#rights+1] = cell end elseif ornament == topornament then setheight(row,0) setdepth(row,0) found = true tops[#tops+1] = cell elseif ornament == bottomornament then setheight(row,0) setdepth(row,0) found = true bottoms[#bottoms+1] = cell end end end end if next(cells) then local c = 0 for n in nextrecord, preamble do c = c + 1 if cells[c] then setwidth(n) end end end end local function second_pass(box,callback,attr,preamble,signal) if found then local head = getlist(nuts.getbox(box)) local leftmargin = texgetdimen("d_math_matrix_margin_l") local rightmargin = texgetdimen("d_math_matrix_margin_r") local topmargin = texgetdimen("d_math_matrix_margin_t") local bottommargin = texgetdimen("d_math_matrix_margin_b") for i=1,#lefts do setoffsets(lefts[i],-(left+leftmargin),0) end for i=1,#rights do setoffsets(rights[i], (rightmargin),0) end for i=1,#tops do setoffsets(tops[i],0,topmargin) end for i=1,#bottoms do setoffsets(bottoms[i],0,-bottommargin) end lefts = false rights = false tops = false bottoms = false texsetdimen("global","d_math_matrix_max_left", left) texsetdimen("global","d_math_matrix_max_right",right) end end function nodes.handlers.mathmatrixornaments(head,where,callback,attr,preamble) -- 0x3 if callback ~= mathmatrix_code then -- nothing to do here elseif where == "preroll" then first_pass(head,callback,attr,preamble,signal) -- elseif where == "wrapup" then -- second_pass(head,callback,attr,preamble,signal) end end interfaces.implement { name = "shiftmatrixornaments", actions = second_pass, arguments = "integer", } local enabled = false interfaces.implement { name = "enablemathalignrelocate", -- onlyonce = true, public = true, protected = true, actions = function() if not enabled then nodes.tasks.enableaction("alignments","nodes.handlers.mathalignrelocate") -- 0x2 enabled = true end end, } local enabled = false interfaces.implement { name = "enablematrixornaments", -- onlyonce = true, public = true, protected = true, actions = function() if not enabled then nodes.tasks.enableaction("alignments","nodes.handlers.mathmatrixornaments") -- 0x3 enabled = true end end, } end -- This only kicks in when we have a callback set. local report = logs.reporter("alignment","preamble") local trace trackers.register("alignments.showstates",function(v) trace = v end) function nodes.handlers.showpreamble(head,where,callback,attr,preamble) if trace then local c = 0 for n, id in nextnode, preamble do if id == unset_code or id == alignrecord_code then c = c + 1 report("stage %a, cell %i, width %p",where,c,getwidth(n)) elseif id == glue_code then report("stage %a, tabskip %s",where,node.direct.gluetostring(n)) else report("stage %a, node %a",where,nodecodes[id]) end end end end