if not modules then modules = { } end modules ['node-rul'] = { version = 1.001, comment = "companion to node-rul.mkiv", author = "Hans Hagen, PRAGMA-ADE, Hasselt NL", copyright = "PRAGMA ADE / ConTeXt Development Team", license = "see context related readme files" } -- this will go to an auxiliary module -- beware: rules now have a dir field -- -- todo: make robust for layers ... order matters local attributes, nodes, node = attributes, nodes, node local nuts = nodes.nuts local tonode = nuts.tonode local tonut = nuts.tonut local getfield = nuts.getfield local setfield = nuts.setfield local setnext = nuts.setnext local setprev = nuts.setprev local setlink = nuts.setlink local getnext = nuts.getnext local getprev = nuts.getprev local getid = nuts.getid local getattr = nuts.getattr local setattr = nuts.setattr local getfont = nuts.getfont local getsubtype = nuts.getsubtype local getlist = nuts.getlist local setlist = nuts.setlist local nodecodes = nodes.nodecodes local tasks = nodes.tasks local properties = nodes.properties local attribs = node.current_attr local glyph_code = nodecodes.glyph local disc_code = nodecodes.disc local rule_code = nodecodes.rule local boundary_code = nodecodes.boundary local dir_code = nodecodes.dir function nodes.striprange(first,last) -- todo: dir if first and last then -- just to be sure if first == last then return first, last end while first and first ~= last do local id = getid(first) if id == glyph_code or id == disc_code or id == dir_code or id == boundary_code then -- or id == rule_code break else first = getnext(first) end end if not first then return nil, nil elseif first == last then return first, last end while last and last ~= first do local id = getid(last) if id == glyph_code or id == disc_code or id == dir_code or id == boundary_code then -- or id == rule_code break else local prev = getprev(last) -- luatex < 0.70 has italic correction kern not prev'd if prev then last = prev else break end end end if not last then return nil, nil end end return first, last end -- todo: order and maybe other dimensions local floor = math.floor local trace_ruled = false trackers.register("nodes.rules", function(v) trace_ruled = v end) local report_ruled = logs.reporter("nodes","rules") local n_tostring = nodes.idstostring local n_tosequence = nodes.tosequence local a_ruled = attributes.private('ruled') local a_color = attributes.private('color') local a_transparency = attributes.private('transparency') local a_colorspace = attributes.private('colormodel') local insert_node_before = nuts.insert_before local insert_node_after = nuts.insert_after local list_dimensions = nuts.dimensions local hpack_nodes = nuts.hpack local striprange = nodes.striprange local fontdata = fonts.hashes.identifiers local variables = interfaces.variables local dimenfactor = fonts.helpers.dimenfactor local splitdimen = number.splitdimen local v_yes = variables.yes local v_all = variables.all local v_foreground = variables.foreground local nodecodes = nodes.nodecodes local skipcodes = nodes.skipcodes local kerncodes = nodes.kerncodes local glyph_code = nodecodes.glyph local disc_code = nodecodes.disc local glue_code = nodecodes.glue local penalty_code = nodecodes.penalty local kern_code = nodecodes.kern local hlist_code = nodecodes.hlist local vlist_code = nodecodes.vlist local rule_code = nodecodes.rule local boundary_code = nodecodes.boundary local dir_code = nodecodes.dir local userskip_code = skipcodes.userskip local spaceskip_code = skipcodes.spaceskip local xspaceskip_code = skipcodes.xspaceskip local leader_code = skipcodes.leaders local kerning_code = kerncodes.kern local nodepool = nuts.pool local new_rule = nodepool.rule local new_userrule = nodepool.userrule local new_kern = nodepool.kern local new_glue = nodepool.glue -- we can use this one elsewhere too -- -- todo: functions: word, sentence -- -- glyph rule unset whatsit glue margin_kern kern math disc local checkdir = true -- we assume {glyphruns} and no funny extra kerning, ok, maybe we need -- a dummy character as start and end; anyway we only collect glyphs -- -- this one needs to take layers into account (i.e. we need a list of -- critical attributes) -- omkeren class en level -> scheelt functie call in analyze -- todo: switching inside math -- handlers local function processwords(attribute,data,flush,head,parent) -- we have hlistdir and local dir local n = head if n then local f, l, a, d, i, class local continue, leaders, done, strip, level = false, false, false, true, -1 while n do local id = getid(n) if id == glyph_code or id == rule_code then local aa = getattr(n,attribute) if aa then if aa == a then if not f then -- ? f = n end l = n else -- possible extensions: when in same class then keep spanning local newlevel, newclass = floor(aa/1000), aa%1000 -- strip = not continue or level == 1 -- 0 if f then if class == newclass then -- and newlevel > level then head, done = flush(head,f,l,d,level,parent,false), true else head, done = flush(head,f,l,d,level,parent,strip), true end end f, l, a = n, n, aa level, class = newlevel, newclass d = data[class] local c = d.continue leaders = c == v_all continue = leaders or c == v_yes end else if f then head, done = flush(head,f,l,d,level,parent,strip), true end f, l, a = nil, nil, nil end elseif id == disc_code or id == boundary_code then if f then l = n end elseif id == kern_code and getsubtype(n) == kerning_code then if f then l = n end elseif id == hlist_code or id == vlist_code then if f then head, done = flush(head,f,l,d,level,parent,strip), true f, l, a = nil, nil, nil end local list = getlist(n) if list then setlist(n,(processwords(attribute,data,flush,list,n))) -- watch () end -- elseif checkdir and id == dir_code then -- only changes in dir, we assume proper boundaries -- if f and a then -- l = n -- end elseif id == dir_code then -- only changes in dir, we assume proper boundaries if f then l = n end elseif f then if continue then if id == penalty_code then l = n -- elseif id == kern_code then -- l = n elseif id == glue_code then -- catch \underbar{a} \underbar{a} (subtype test is needed) local subtype = getsubtype(n) if getattr(n,attribute) and (subtype == userskip_code or subtype == spaceskip_code or subtype == xspaceskip_code or (leaders and subtype >= leader_code)) then l = n else head, done = flush(head,f,l,d,level,parent,strip), true f, l, a = nil, nil, nil end end else head, done = flush(head,f,l,d,level,parent,strip), true f, l, a = nil, nil, nil end end n = getnext(n) end if f then head, done = flush(head,f,l,d,level,parent,strip), true end return head, true -- todo: done else return head, false end end -- nodes.processwords = processwords nodes.processwords = function(attribute,data,flush,head,parent) -- we have hlistdir and local dir head = tonut(head) if parent then parent = tonut(parent) end local head, done = processwords(attribute,data,flush,head,parent) return tonode(head), done end -- local rules = nodes.rules or { } nodes.rules = rules rules.data = rules.data or { } storage.register("nodes/rules/data", rules.data, "nodes.rules.data") local data = rules.data -- we implement user rules here as it takes less code this way local function userrule(t,noattributes) local r = new_userrule(t.width or 0,t.height or 0,t.depth or 0) if noattributes == false or noattributes == nil then -- avoid fuzzy ones else setfield(r,"attr",attribs()) end properties[r] = t return tonode(r) end rules.userrule = userrule local ruleactions = { } rules.ruleactions = ruleactions callback.register("process_rule",function(n,h,v) local n = tonut(n) local p = properties[n] if p then local i = p.type or "draw" local a = ruleactions[i] if a then a(p,h,v,i,n) end end end) -- function rules.define(settings) data[#data+1] = settings context(#data) end local a_viewerlayer = attributes.private("viewerlayer") local function flush_ruled(head,f,l,d,level,parent,strip) -- not that fast but acceptable for this purpose if getid(f) ~= glyph_code then -- saveguard ... we need to deal with rules and so (math) return head end local r, m if strip then if trace_ruled then local before = n_tosequence(f,l,true) f, l = striprange(f,l) local after = n_tosequence(f,l,true) report_ruled("range stripper, before %a, after %a",before,after) else f, l = striprange(f,l) end end if not f then return head end local w, ht, dp = list_dimensions(getfield(parent,"glue_set"),getfield(parent,"glue_sign"),getfield(parent,"glue_order"),f,getnext(l)) local method = d.method local offset = d.offset local continue = d.continue local dy = d.dy local order = d.order local max = d.max local mp = d.mp local rulethickness = d.rulethickness local unit = d.unit local ma = d.ma local ca = d.ca local ta = d.ta local colorspace = ma > 0 and ma or getattr(f,a_colorspace) or 1 local color = ca > 0 and ca or getattr(f,a_color) local transparency = ta > 0 and ta or getattr(f,a_transparency) local foreground = order == v_foreground local layer = getattr(f,a_viewerlayer) local e = dimenfactor(unit,getfont(f)) -- what if no glyph node local rt = tonumber(rulethickness) if rt then rulethickness = e * rulethickness / 2 else local n, u = splitdimen(rulethickness) if n and u then -- we need to intercept ex and em and % and ... rulethickness = n * dimenfactor(u,fontdata[getfont(f)]) / 2 else rulethickness = 1/5 end end -- if level > max then level = max end if method == 0 then -- center offset = 2*offset m = (offset+(level-1)*dy)*e/2 + rulethickness/2 else m = 0 end local function inject(r,w,ht,dp) if layer then setattr(r,a_viewerlayer,layer) end local k = new_kern(-w) if foreground then insert_node_after(head,l,k) insert_node_after(head,k,r) l = r else head = insert_node_before(head,f,r) insert_node_after(head,r,k) end if trace_ruled then report_ruled("level %a, width %p, height %p, depth %p, nodes %a, text %a", level,w,ht,dp,n_tostring(f,l),n_tosequence(f,l,true)) end end if mp and mp ~= "" then local r = userrule { width = w, height = ht, depth = dp, type = "mp", factor = e, offset = offset, line = rulethickness, data = mp, ma = colorspace, ca = color, ta = transparency, } inject(tonut(r),w,ht,dp) else for i=1,level do local ht = (offset+(i-1)*dy)*e + rulethickness - m local dp = -(offset+(i-1)*dy)*e + rulethickness + m local r = new_rule(w,ht,dp) if color then setattr(r,a_colorspace,colorspace) setattr(r,a_color,color) end if transparency then setattr(r,a_transparency,transparency) end inject(r,w,ht,dp) end end return head end local process = nodes.processwords rules.handler = function(head) return process(a_ruled,data,flush_ruled,head) end function rules.enable() tasks.enableaction("shipouts","nodes.rules.handler") end -- elsewhere: -- -- tasks.appendaction ("shipouts", "normalizers", "nodes.rules.handler") -- tasks.disableaction("shipouts", "nodes.rules.handler") -- only kick in when used local trace_shifted = false trackers.register("nodes.shifting", function(v) trace_shifted = v end) local report_shifted = logs.reporter("nodes","shifting") local a_shifted = attributes.private('shifted') nodes.shifts = nodes.shifts or { } nodes.shifts.data = nodes.shifts.data or { } storage.register("nodes/shifts/data", nodes.shifts.data, "nodes.shifts.data") local data = nodes.shifts.data function nodes.shifts.define(settings) data[#data+1] = settings context(#data) end local function flush_shifted(head,first,last,data,level,parent,strip) -- not that fast but acceptable for this purpose if true then first, last = striprange(first,last) end local prev = getprev(first) local next = getnext(last) setprev(first) setnext(last) local width, height, depth = list_dimensions(getfield(parent,"glue_set"),getfield(parent,"glue_sign"),getfield(parent,"glue_order"),first,next) local list = hpack_nodes(first,width,"exactly") if first == head then head = list end if prev then setlink(prev,list) end if next then setlink(list,next) end local raise = data.dy * dimenfactor(data.unit,fontdata[getfont(first)]) setfield(list,"shift",raise) setfield(list,"height",height) setfield(list,"depth",depth) if trace_shifted then report_shifted("width %p, nodes %a, text %a",width,n_tostring(first,last),n_tosequence(first,last,true)) end return head end local process = nodes.processwords nodes.shifts.handler = function(head) return process(a_shifted,data,flush_shifted,head) end function nodes.shifts.enable() tasks.enableaction("shipouts","nodes.shifts.handler") end -- linefillers (placeholder) nodes.linefillers = nodes.linefillers or { } nodes.linefillers.data = nodes.linefillers.data or { } function nodes.linefillers.handler(head) return head, false end -- interface local implement = interfaces.implement implement { name = "definerule", actions = { rules.define, context }, arguments = { { { "continue" }, { "unit" }, { "order" }, { "method", "integer" }, { "offset", "number" }, { "rulethickness", "string" }, { "dy", "number" }, { "max", "number" }, { "ma", "integer" }, { "ca", "integer" }, { "ta", "integer" }, { "mp", "string" }, } } } implement { name = "enablerules", onlyonce = true, actions = rules.enable } implement { name = "defineshift", actions = { nodes.shifts.define, context }, arguments = { { { "continue" }, { "unit" }, { "method", "integer" }, { "dy", "number" }, } } } implement { name = "enableshifts", onlyonce = true, actions = nodes.shifts.enable }