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