typo-fln.lua /size: 13 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['typo-fln'] = {
2    version   = 1.001,
3    comment   = "companion to typo-fln.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-- When I ran into the following experimental code again, I figured that it dated
10-- from the early days of mkiv, so I updates it a bit to fit into todays context.
11-- In the process I might have messed up things. For instance we had a diffent
12-- wrapper then using head and tail.
13
14-- todo: only letters (no punctuation)
15-- todo: nuts
16
17local trace_firstlines   = false  trackers.register("typesetters.firstlines", function(v) trace_firstlines = v end)
18local report_firstlines  = logs.reporter("nodes","firstlines")
19
20typesetters.firstlines   = typesetters.firstlines or { }
21local firstlines         = typesetters.firstlines
22
23local nodes              = nodes
24
25local tasks              = nodes.tasks
26local enableaction       = tasks.enableaction
27local disableaction      = tasks.disableaction
28
29local context            = context
30local implement          = interfaces.implement
31
32local nuts               = nodes.nuts
33local tonode             = nuts.tonode
34
35local getnext            = nuts.getnext
36local getprev            = nuts.getprev
37local getboth            = nuts.getboth
38local setboth            = nuts.setboth
39local getid              = nuts.getid
40local getwidth           = nuts.getwidth
41local getlist            = nuts.getlist
42local setlist            = nuts.setlist
43local getattr            = nuts.getattr
44local setattr            = nuts.setattr
45local getbox             = nuts.getbox
46local getdisc            = nuts.getdisc
47local setdisc            = nuts.setdisc
48local setlink            = nuts.setlink
49local setfont            = nuts.setfont
50local setglyphdata       = nuts.setglyphdata
51local getprop            = nuts.getprop
52local setprop            = nuts.setprop
53
54local nodecodes          = nodes.nodecodes
55local glyph_code         = nodecodes.glyph
56local disc_code          = nodecodes.disc
57local kern_code          = nodecodes.kern
58local glue_code          = nodecodes.glue
59local par_code           = nodecodes.par
60
61local spaceskip_code     = nodes.gluecodes.spaceskip
62
63local nextglyph          = nuts.traversers.glyph
64local nextdisc           = nuts.traversers.disc
65
66local flushnodelist      = nuts.flushlist
67local flushnode          = nuts.flushnode
68local copy_node_list     = nuts.copylist
69local insertnodebefore   = nuts.insertbefore
70local insertnodeafter    = nuts.insertafter
71local remove_node        = nuts.remove
72local getdimensions      = nuts.dimensions
73local hpack_node_list    = nuts.hpack
74local startofpar         = nuts.startofpar
75
76local nodepool           = nuts.pool
77local newpenalty         = nodepool.penalty
78local newkern            = nodepool.kern
79local tracerrule         = nodes.tracers.pool.nuts.rule
80
81local actions            = { }
82firstlines.actions       = actions
83
84local a_firstline        = attributes.private('firstline')
85local a_color            = attributes.private('color')
86local a_transparency     = attributes.private('transparency')
87local a_colormodel       = attributes.private('colormodel')
88
89local texget             = tex.get
90
91local variables          = interfaces.variables
92local v_default          = variables.default
93local v_line             = variables.line
94local v_word             = variables.word
95
96local function set(par,specification)
97    enableaction("processors","typesetters.firstlines.handler")
98    if trace_firstlines then
99        report_firstlines("enabling firstlines")
100    end
101    setprop(par,a_firstline,specification)
102end
103
104function firstlines.set(specification)
105    nuts.setparproperty(set,specification)
106end
107
108implement {
109    name      = "setfirstline",
110    actions   = firstlines.set,
111    arguments = {
112        {
113            { "alternative" },
114            { "font", "integer" },
115            { "dynamic", "integer" },
116            { "ma", "integer" },
117            { "ca", "integer" },
118            { "ta", "integer" },
119            { "n", "integer" },
120        }
121    }
122}
123
124actions[v_line] = function(head,setting)
125    local dynamic    = setting.dynamic
126    local font       = setting.font
127    local noflines   = setting.n or 1
128    local ma         = setting.ma or 0
129    local ca         = setting.ca
130    local ta         = setting.ta
131    local hangafter  = texget("hangafter")
132    local hangindent = texget("hangindent")
133    local parindent  = texget("parindent")
134    local nofchars   = 0
135    local n          = 0
136    local temp       = copy_node_list(head)
137    local linebreaks = { }
138
139    local set = function(head)
140        for g in nextglyph, head do
141            if dynamic > 0 then
142                setglyphdata(g,dynamic)
143            end
144            setfont(g,font)
145        end
146    end
147
148    set(temp)
149
150    for g in nextdisc, temp do
151        local pre, post, replace = getdisc(g)
152        if pre then
153            set(pre)
154        end
155        if post then
156            set(post)
157        end
158        if replace then
159            set(replace)
160        end
161    end
162
163    local start = temp
164    local list  = temp
165    local prev  = temp
166    for i=1,noflines do
167        local hsize = texget("hsize") - texget("leftskip",false) - texget("rightskip",false)
168        if i == 1 then
169            hsize = hsize - parindent
170        end
171        if i <= - hangafter then
172            hsize = hsize - hangindent
173        end
174
175        local function list_dimensions(list,start)
176            local temp = copy_node_list(list,start)
177            temp = nodes.handlers.characters(temp)
178            temp = nodes.injections.handler(temp)
179         -- temp = typesetters.fontkerns.handler(temp) -- maybe when enabled
180         --        nodes.handlers.protectglyphs(temp)  -- not needed as we discard
181         -- temp = typesetters.spacings.handler(temp)  -- maybe when enabled
182         -- temp = typesetters.kerns.handler(temp)     -- maybe when enabled
183         -- temp = typesetters.cases.handler(temp)     -- maybe when enabled
184            local width = getdimensions(temp)
185            flushnodelist(temp)
186            return width
187        end
188
189        local function try(extra)
190            local width = list_dimensions(list,start)
191            if extra then
192                width = width + list_dimensions(extra)
193            end
194         -- report_firstlines("line length: %p, progression: %p, text: %s",hsize,width,nodes.listtoutf(list,nil,nil,start))
195            if width > hsize then
196                list = prev
197                return true
198            else
199                linebreaks[i] = n
200                prev = start
201                nofchars = n
202            end
203        end
204
205        while start do
206            local id = getid(start)
207            if id == glyph_code then
208                -- go on
209            elseif id == disc_code then
210                -- this could be an option
211                n = n + 1
212                local pre, post, replace = getdisc(start)
213                if pre and try(pre) then
214                    break
215                elseif replace and try(replace) then
216                    break
217                end
218            elseif id == kern_code then -- todo: fontkern
219                -- this could be an option
220            elseif id == glue_code then
221                n = n + 1
222                if try() then
223                    break
224                end
225            end
226            start = getnext(start)
227        end
228        if not linebreaks[i] then
229            linebreaks[i] = n
230        end
231    end
232
233    flushnodelist(temp)
234
235    local start = head
236    local n     = 0
237
238    local function update(start)
239        if dynamic > 0 then
240            setglyphdata(start,dynamic)
241        end
242        setfont(start,font)
243        if ca and ca > 0 then
244            setattr(start,a_colormodel,ma == 0 and 1 or ma)
245            setattr(start,a_color,ca)
246        end
247        if ta and ta > 0 then
248            setattr(start,a_transparency,ta)
249        end
250    end
251
252    for i=1,noflines do
253        local linebreak = linebreaks[i]
254        while start and n < nofchars do
255            local id = getid(start)
256            local ok = false
257            if id == glyph_code then
258                update(start)
259            elseif id == disc_code then
260                n = n + 1
261                local disc = start
262                local pre, post, replace, pretail, posttail, replacetail = getdisc(disc,true)
263                if linebreak == n then
264                    local p, n = getboth(start)
265                    if pre then
266                        for current in nextglyph, pre do
267                            update(current)
268                        end
269                        setlink(pretail,n)
270                        setlink(p,pre)
271                        start = pretail
272                        pre = nil
273                    else
274                        setlink(p,n)
275                        start = p
276                    end
277                    if post then
278                        local p, n = getboth(start)
279                        setlink(posttail,n)
280                        setlink(start,post)
281                        post = nil
282                    end
283                else
284                    local p, n = getboth(start)
285                    if replace then
286                        for current in nextglyph, replace do
287                            update(current)
288                        end
289                        setlink(replacetail,n)
290                        setlink(p,replace)
291                        start = replacetail
292                        replace = nil
293                    else
294                        setlink(p,n)
295                        start = p
296                    end
297                end
298                setdisc(disc,pre,post,replace)
299                flushnode(disc)
300            elseif id == glue_code then
301                n = n + 1
302                if linebreak ~= n then
303                    head = insertnodebefore(head,start,newpenalty(10000)) -- nobreak
304                end
305            end
306            local next = getnext(start)
307            if linebreak == n then
308                if start ~= head then
309                    local where = id == glue_code and getprev(start) or start
310                    if trace_firstlines then
311                        head, where = insertnodeafter(head,where,newpenalty(10000)) -- nobreak
312                        head, where = insertnodeafter(head,where,newkern(-65536))
313                        head, where = insertnodeafter(head,where,tracerrule(65536,4*65536,2*65536,"darkblue"))
314                    end
315                    head, where = insertnodeafter(head,where,newpenalty(-10000)) -- break
316                end
317                start = next
318                break
319            end
320            start = next
321        end
322    end
323
324    return head
325end
326
327actions[v_word] = function(head,setting)
328 -- local attribute = fonts.specifiers.contextnumber(setting.feature) -- was experimental
329    local dynamic  = setting.dynamic
330    local font     = setting.font
331    local words    = 0
332    local nofwords = setting.n or 1
333    local start    = head
334    local ok       = false
335    local ma       = setting.ma or 0
336    local ca       = setting.ca
337    local ta       = setting.ta
338    while start do
339        local id = getid(start)
340        -- todo: delete disc nodes
341        if id == glyph_code then
342            if not ok then
343                words = words + 1
344                ok = true
345            end
346            if ca and ca > 0 then
347                setattr(start,a_colormodel,ma == 0 and 1 or ma)
348                setattr(start,a_color,ca)
349            end
350            if ta and ta > 0 then
351                setattr(start,a_transparency,ta)
352            end
353            if dynamic > 0 then
354                setglyphdata(start,dynamic)
355            end
356            setfont(start,font)
357        elseif id == disc_code then
358            -- continue
359        elseif id == kern_code then -- todo: fontkern
360            -- continue
361        else
362            ok = false
363            if words == nofwords then
364                break
365            end
366        end
367        start = getnext(start)
368    end
369    return head
370end
371
372actions[v_default] = actions[v_line]
373
374function firstlines.handler(head)
375    if getid(head) == par_code and startofpar(head) then
376        local settings = getprop(head,a_firstline)
377        if settings then
378            disableaction("processors","typesetters.firstlines.handler")
379            local alternative = settings.alternative or v_default
380            local action = actions[alternative] or actions[v_default]
381            if action then
382                if trace_firstlines then
383                    report_firstlines("processing firstlines, alternative %a",alternative)
384                end
385                return action(head,settings)
386            end
387        end
388    end
389    return head
390end
391
392-- goodie
393
394local function applytofirstcharacter(box,what)
395    local tbox = getbox(box) -- assumes hlist
396    local list = getlist(tbox)
397    local done = nil
398    for n in nextglyph, list do
399        list = remove_node(list,n)
400        done = n
401        break
402    end
403    if done then
404        setlist(tbox,list)
405        local kind = type(what)
406        if kind == "string" then
407            context[what](tonode(done))
408        elseif kind == "function" then
409            what(done)
410        else
411            -- error
412        end
413    end
414end
415
416implement {
417    name      = "applytofirstcharacter",
418    actions   = applytofirstcharacter,
419    arguments = { "integer", "string" }
420}
421