typo-drp.lmt /size: 15 Kb    last modification: 2025-02-21 11:03
1if not modules then modules = { } end modules ['typo-drp'] = {
2    version   = 1.001,
3    comment   = "companion to typo-drp.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files"
7}
8
9-- This one is sensitive for order (e.g. when combined with first line
10-- processing.
11
12-- todo: use isglyph
13
14local tonumber, type, next = tonumber, type, next
15local ceil = math.ceil
16local settings_to_hash = utilities.parsers.settings_to_hash
17
18local trace_initials    = false  trackers.register("typesetters.initials", function(v) trace_initials = v end)
19local report_initials   = logs.reporter("nodes","initials")
20
21local initials          = typesetters.initials or { }
22typesetters.initials    = initials or { }
23
24local nodes             = nodes
25
26local tasks             = nodes.tasks
27local enableaction      = tasks.enableaction
28local disableaction     = tasks.disableaction
29
30local nuts              = nodes.nuts
31local tonut             = nodes.tonut
32
33local getnext           = nuts.getnext
34local getprev           = nuts.getprev
35local getchar           = nuts.getchar
36local getid             = nuts.getid
37local getattr           = nuts.getattr
38local getwhd            = nuts.getwhd
39
40local getprop           = nuts.getprop
41local setprop           = nuts.setprop
42
43local setlink           = nuts.setlink
44local setprev           = nuts.setprev
45local setnext           = nuts.setnext
46local setfont           = nuts.setfont
47local setscale          = nuts.setscale
48local setwhd            = nuts.setwhd
49local setkern           = nuts.setkern
50local setoffsets        = nuts.setoffsets
51local setglyphdata      = nuts.setglyphdata
52local getparstate       = nuts.getparstate
53local patchparshape     = nuts.patchparshape
54
55local hpack_nodes       = nuts.hpack
56
57local nodepool          = nuts.pool
58local new_kern          = nodepool.kern
59
60local insertbefore      = nuts.insertbefore
61local insertafter       = nuts.insertafter
62local remove_node       = nuts.remove
63
64local startofpar        = nuts.startofpar
65
66local nextnode          = nuts.traversers.node
67local nextglyph         = nuts.traversers.glyph
68
69local setcoloring       = nuts.colors.set
70
71local variables         = interfaces.variables
72local v_default         <const> = variables.default
73local v_margin          <const> = variables.margin
74local v_auto            <const> = variables.auto
75local v_first           <const> = variables.first
76local v_keep            <const> = variables.keep
77local v_yes             <const> = variables.yes
78local v_last            <const> = variables.last
79
80local texget            = tex.get
81local texset            = tex.set
82
83local unsetvalue        <const> = attributes.unsetvalue
84
85local nodecodes         = nodes.nodecodes
86local glyph_code        <const> = nodecodes.glyph
87local hlist_code        <const> = nodecodes.hlist
88local glue_code         <const> = nodecodes.glue
89local kern_code         <const> = nodecodes.kern
90local par_code          <const> = nodecodes.par
91
92local actions           = { }
93local busyactions       = { }
94initials.actions        = actions
95initials.busyactions    = actions
96
97local a_initial         <const> = attributes.private("initial")
98
99local category          = characters.category
100
101local levels            = { }
102local getlevel          = tex.getnestlevel or function() return tex.getnest("prv") end
103
104local function set(par,specification)
105    enableaction("processors","typesetters.initials.handler")
106    if trace_initials then
107        report_initials("enabling initials")
108    end
109    levels[getlevel()] = true
110    setprop(par,a_initial,specification)
111end
112
113function initials.set(specification)
114    nuts.setparproperty(set,specification)
115end
116
117interfaces.implement {
118    name      = "setinitial",
119    actions   = initials.set,
120    arguments = {
121        {
122            { "location" },
123            { "enabled", "boolean" },
124            { "method" },
125            { "continue" },
126            { "distance" ,"dimension" },
127            { "hoffset" ,"dimension" },
128            { "voffset" ,"dimension" },
129            { "font", "integer" },
130            { "glyphscale", "integer" },
131            { "dynamic", "integer" },
132            { "ca", "integer" },
133            { "ma", "integer" },
134            { "ta", "integer" },
135            { "n", "integer" },
136            { "m", "integer" },
137        }
138    }
139}
140
141-- todo: prevent linebreak .. but normally a initial ends up at the top of
142-- a page so this has a low priority
143
144local function updateshapestate(head,pdata,hangafter,hangindent)
145    local parshape = pdata.parshape
146    if parshape then
147        local size = #parshape
148        if size > 0 then
149            local hang = -hangafter
150            local last = hang >= size and { parshape[size][1], parshape[size][2] } or false
151            for i=1,hang do
152                local p = parshape[i]
153                if p then
154                    p[1] = p[1] + hangindent
155                    p[2] = p[2] - hangindent
156                else
157                    parshape[i] = {
158                        last[1] + hangindent,
159                        last[2] - hangindent,
160                    }
161                end
162            end
163            if last then
164                parshape[hang+1] = last
165            end
166            patchparshape(head,parshape)
167            return
168        end
169    end
170    texset("hangafter",hangafter,true)
171    texset("hangindent",hangindent,true)
172end
173
174actions[v_default] = function(head,settings)
175    local skip = false
176    local busy = false
177    -- begin of par
178    local first  = getnext(head)
179    local indent = false
180    -- parbox .. needs to be set at 0
181    if first and getid(first) == hlist_code then
182        first  = getnext(first)
183        indent = true
184    end
185    -- we need to skip over kerns and glues (signals)
186    while first and getid(first) ~= glyph_code do
187        first = getnext(first)
188    end
189    if first and getid(first) == glyph_code then
190        local ma        = settings.ma or 0
191        local ca        = settings.ca
192        local ta        = settings.ta
193        local last      = first
194        local distance  = settings.distance or 0
195        local voffset   = settings.voffset or 0
196        local hoffset   = settings.hoffset or 0
197        local parindent = texget("parindent")
198        local baseline  = texget("baselineskip",false)
199        local lines     = tonumber(settings.n) or 0
200        local dynamic   = settings.dynamic
201        local font      = settings.font
202        local scale     = settings.glyphscale
203        local method    = settings_to_hash(settings.method)
204        local length    = tonumber(settings.m) or 1
205        --
206        -- 1 char | n chars | skip first quote | ignore punct | keep punct
207        --
208        if getattr(first,a_initial) then
209            for current in nextnode, getnext(first) do
210                if getattr(current,a_initial) then
211                    last = current
212                else
213                    break
214                end
215            end
216        elseif method[v_auto] then
217            local char = getchar(first)
218            local kind = category(char)
219            if kind == "po" or kind == "pi" then
220                if method[v_first] then
221                    -- remove quote etc before initial
222                    local next = getnext(first)
223                    if not next then
224                        -- don't start with a quote or so
225                        return head
226                    end
227                    last = nil
228                    for current in nextglyph, next do
229                        head, first = remove_node(head,first,true)
230                        first = current
231                        last = first
232                        break
233                    end
234                    if not last then
235                        -- no following glyph or so
236                        return head
237                    end
238                else
239                    -- keep quote etc with initial
240                    local next = getnext(first)
241                    if next and method[v_keep] then
242                        skip = first
243                    end
244                    if not next then
245                        -- don't start with a quote or so
246                        return head
247                    end
248                    for current in nextglyph, next do
249                        last = current
250                        break
251                    end
252                    if last == first then
253                        return head
254                    end
255                end
256            elseif kind == "pf" then
257                -- error: final quote
258            else
259                -- okay
260            end
261            -- maybe also: get all A. B. etc
262            local next = getnext(first)
263            if next then
264                for current, char in nextglyph, next do
265                    local kind = category(char)
266                    if kind == "po" then
267                        if method[v_last] then
268                            -- remove period etc after initial
269                            remove_node(head,current,true)
270                        else
271                            -- keep period etc with initial
272                            last = current
273                        end
274                    end
275                    break
276                end
277            end
278        else
279            for current in nextglyph, first do
280                last = current
281                if length <= 1 then
282                    break
283                else
284                    length = length - 1
285                end
286            end
287        end
288        local current = first
289        while true do
290            local id = getid(current)
291            if id == kern_code then
292                setkern(current,0)
293            elseif id == glyph_code and skip ~= current then
294                local next = getnext(current)
295                if font then
296                    setfont(current,font)
297                end
298                if scale then
299                    setscale(current,scale)
300                end
301                if dynamic > 0 then
302                    setglyphdata(current,dynamic)
303                end
304                setcoloring(current,ma,ca,ta)
305            end
306            if current == last then
307                break
308            else
309                current = getnext(current)
310            end
311        end
312        -- We pack so that successive handling cannot touch the dropped cap. Packaging
313        -- in a hlist is also needed because we cannot locally adapt e.g. parindent (not
314        -- yet stored in with par).
315        local prev = getprev(first)
316        local next = getnext(last)
317        --
318        setprev(first)
319        setnext(last)
320        local dropper = hpack_nodes(first)
321        local width, height, depth = getwhd(dropper)
322        setwhd(dropper,0,0,0)
323        --
324        setlink(prev,dropper,next)
325        --
326        if next then
327            local current = next
328            while current do
329                local id = getid(current)
330                if id == glue_code or id == kern_code then
331                    local next = getnext(current)
332                 -- remove_node(current,current,true) -- created an invalid next link and dangling remains
333                    remove_node(head,current,true)
334                    current = next
335                else
336                    break
337                end
338            end
339        end
340        --
341        local hoffset = width + hoffset + distance + (indent and parindent or 0)
342        for current in nextglyph, first do
343            if skip == current then
344                setoffsets(current,-hoffset,0)
345            else
346                setoffsets(current,-hoffset,-voffset) -- no longer - height here
347            end
348            if current == last then
349                break
350            end
351        end
352        --
353        first = dropper
354        --
355        if settings.location == v_margin then
356            -- okay
357        else
358            if lines == 0 then -- safeguard, not too precise
359                lines = ceil((height+voffset) / baseline)
360            end
361            -- We cannot set parshape yet ... when we can I'll add a slope
362            -- option (positive and negative, in emwidth).
363            local hangafter  = - lines
364            local hangindent = width + distance
365            if trace_initials then
366                report_initials("setting hangafter to %i and hangindent to %p",hangafter,hangindent)
367            end
368            if settings.continue == v_yes then
369                local state = {
370                    lines      = lines,
371                    hangindent = hangindent,
372                    parindent  = indent and parindent,
373                }
374                busy = function(head)
375                    local lines = state.lines
376                    local pdata = getparstate(head,true)
377                    local done  = pdata.prevgraf or lines
378                    if done < lines then
379                        local hangafter  = -(lines - done)
380                        local hangindent = state.hangindent
381                        updateshapestate(head,pdata,hangafter,hangindent)
382-- texset("hangafter",hangafter,true)
383-- texset("hangindent",hangindent,true)
384                        if state.parindent then
385                            insertafter(first,first,new_kern(-state.parindent))
386                        end
387                        state.lines =  lines - done
388                        return busy
389                    else
390                        return false
391                    end
392                end
393            end
394            local pdata = getparstate(head,true)
395            updateshapestate(head,pdata,hangafter,hangindent)
396        end
397        if indent then
398            insertafter(first,first,new_kern(-parindent))
399        end
400    end
401    return head, busy
402end
403
404-- we can count ... when all done, we can disable ...
405
406-- function initials.handler(head,groupcode)
407--     if getid(head) == par_code and startofpar(head) then
408--         local settings = getprop(head,a_initial)
409--         if settings then
410--             disableaction("processors","typesetters.initials.handler")
411--             local alternative = settings.alternative or v_default
412--             local action = actions[alternative] or actions[v_default]
413--             if action then
414--                 if trace_initials then
415--                     report_initials("processing initials, alternative %a",alternative)
416--                 end
417--                 return action(head,settings)
418--             end
419--         end
420--     end
421--     return head
422-- end
423
424-- doesn't work well nested but no sane user will do that
425
426local busy = false
427
428function initials.handler(head,groupcode)
429    if getid(head) == par_code and startofpar(head) then
430        if busy then
431            busy = busy(head)
432        end
433        if not busy then
434            local settings = getprop(head,a_initial)
435            if not settings or settings.continue ~= v_yes then
436                levels[getlevel()] = nil
437                if not next(levels) then
438                    disableaction("processors","typesetters.initials.handler")
439                    if trace_initials then
440                        report_initials("disabling initials")
441                    end
442                end
443            end
444            if settings then
445                local alternative = settings.alternative or v_default
446                local action = actions[alternative] or actions[v_default]
447                if action then
448                    if trace_initials then
449                        report_initials("processing initials, alternative %a",alternative)
450                    end
451                    head, busy = action(head,settings)
452                    return head
453                end
454            end
455        end
456    end
457    return head
458end
459
460