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