summaryrefslogtreecommitdiff
path: root/tex/context/base/mkxl/node-rul.lmt
blob: 3bf18ccf3716930a10552a1b2b27a20b28dc79bf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
if not modules then modules = { } end modules ['node-rul'] = {
    version   = 1.001,
    optimize  = true,
    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

-- todo: collect successive bit and pieces and combine them
--
-- path s ; s := shaped(p) ; % p[] has rectangles
-- fill s withcolor .5white ;
-- draw boundingbox s withcolor yellow;

local tonumber           = tonumber

local context            = context
local attributes         = attributes
local nodes              = nodes
local properties         = nodes.properties.data

local enableaction       = nodes.tasks.enableaction

local nuts               = nodes.nuts
local tonode             = nuts.tonode
local tonut              = nuts.tonut

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 getdirection       = nuts.getdirection
local getattr            = nuts.getattr
local setattr            = nuts.setattr
local setattrs           = nuts.setattrs
local getfont            = nuts.getfont
local getsubtype         = nuts.getsubtype
local getlist            = nuts.getlist
local setwhd             = nuts.setwhd
local setattrlist        = nuts.setattrlist
local setshift           = nuts.setshift
local getwidth           = nuts.getwidth
local setwidth           = nuts.setwidth
local setoffsets         = nuts.setoffsets
local setfield           = nuts.setfield
local getruledata        = nuts.getruledata

local isglyph            = nuts.isglyph

local flushlist          = nuts.flushlist
local effectiveglue      = nuts.effectiveglue
local insertnodeafter    = nuts.insertafter
local insertnodebefore   = nuts.insertbefore
local find_tail          = nuts.tail
local setglue            = nuts.setglue
local getrangedimensions = nuts.rangedimensions
local hpack_nodes        = nuts.hpack
local copylist           = nuts.copylist

local nextlist           = nuts.traversers.list
local nextglue           = nuts.traversers.glue

local nodecodes          = nodes.nodecodes
local rulecodes          = nodes.rulecodes
local gluecodes          = nodes.gluecodes
local listcodes          = nodes.listcodes

local glyph_code         = nodecodes.glyph
local par_code           = nodecodes.par
local dir_code           = nodecodes.dir
local glue_code          = nodecodes.glue
local hlist_code         = nodecodes.hlist

local indentlist_code    = listcodes.indent
local linelist_code      = listcodes.line

local leftskip_code         = gluecodes.leftskip
local rightskip_code        = gluecodes.rightskip
local parfillleftskip_code  = gluecodes.parfillleftskip
local parfillrightskip_code = gluecodes.parfillrightskip
local indentskip_code       = gluecodes.indentskip

local nodepool           = nuts.pool

local new_rule           = nodepool.rule
local new_userrule       = nodepool.userrule
local new_kern           = nodepool.kern
local new_leader         = nodepool.leader

local n_tostring         = nodes.idstostring
local n_tosequence       = nodes.tosequence

local variables          = interfaces.variables
local implement          = interfaces.implement

local privateattributes  = attributes.private

local a_ruled            = privateattributes('ruled')
local a_runningtext      = privateattributes('runningtext')
local a_color            = privateattributes('color')
local a_transparency     = privateattributes('transparency')
local a_colormodel       = privateattributes('colormodel')
local a_linefiller       = privateattributes("linefiller")
local a_viewerlayer      = privateattributes("viewerlayer")

local registervalue      = attributes.registervalue
local getvalue           = attributes.getvalue
local texsetattribute    = tex.setattribute

local v_both             = variables.both
local v_left             = variables.left
local v_right            = variables.right
local v_local            = variables["local"]
local v_yes              = variables.yes
local v_foreground       = variables.foreground

local fonthashes         = fonts.hashes
local fontdata           = fonthashes.identifiers
local fontresources      = fonthashes.resources

local dimenfactor        = fonts.helpers.dimenfactor
local splitdimen         = number.splitdimen
local setmetatableindex  = table.setmetatableindex

local runningrule        = tex.magicconstants.runningrule

local striprange         = nuts.striprange
local processwords       = nuts.processwords

local setcoloring        = nuts.colors.set

do

    local rules              = nodes.rules or { }
    nodes.rules              = rules
    -- rules.data               = rules.data  or { }

    local nutrules           = nuts.rules or { }
    nuts.rules               = nutrules -- not that many

    -- we implement user rules here as it takes less code this way

    local function usernutrule(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
            setattrlist(r,true)
        end
        properties[r] = t
        return r
    end

    nutrules.userrule = usernutrule

    local function userrule(t,noattributes)
        return tonode(usernutrule(t,noattributes))
    end

    rules.userrule       = userrule
    local ruleactions    = { }

    rules   .ruleactions = ruleactions
    nutrules.ruleactions = ruleactions -- convenient

    local function mathaction(n,h,v,what)
        local font    = getruledata(n)
        local actions = fontresources[font].mathruleactions
        if actions then
            local action = actions[what]
            if action then
                action(n,h,v,font)
            end
        end
    end

    local function mathradical(n,h,v)
        mathaction(n,h,v,"radicalaction")
    end

    local function mathrule(n,h,v)
        mathaction(n,h,v,"hruleaction")
    end

    local x

    local function useraction(n,h,v)
        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

    local subtypeactions = {
        [rulecodes.user]     = useraction,
        [rulecodes.over]     = mathrule,
        [rulecodes.under]    = mathrule,
        [rulecodes.fraction] = mathrule,
        [rulecodes.radical]  = mathradical,
    }

    function rules.process(n,h,v)
        local n = tonut(n) -- already a nut
        local s = getsubtype(n)
        local a = subtypeactions[s]
        if a then
            a(n,h,v)
        end
    end

    local trace_ruled   = false  trackers.register("nodes.rules", function(v) trace_ruled = v end)
    local report_ruled  = logs.reporter("nodes","rules")
    local enabled       = false

    local texgetattribute = tex.getattribute
    local unsetvalue      = attributes.unsetvalue

    -- The setter is now the performance bottleneck but it is no longer
    -- limited to a certain number of cases before we cycle resources.

    function rules.set(settings)
        if not enabled then
            enableaction("shipouts","nodes.rules.handler")
            enabled = true
        end
        local text = settings.text
        if text then
            settings.text = tonut(text)
         -- nodepool.register(text) -- todo: have a cleanup hook
        end
        -- todo: only when explicitly enabled
        local attr = texgetattribute(a_ruled)
        if attr ~= unsetvalue then
            settings.nestingvalue = attr
            settings.nestingdata  = getvalue(a_ruled,attr) -- so still accessible when we wipe
        end
        texsetattribute(a_ruled,registervalue(a_ruled,settings))
    end

    attributes.setcleaner(a_ruled,function(t)
        local text = t.text
        if text then
            flushlist(text)
        end
    end)

    -- we could check the passed level on the real one ... (no need to pass level)

    local function flush_ruled(head,f,l,d,level,parent,strip) -- not that fast but acceptable for this purpose
        local level = d.stack or d.level or 1
        if level > d.max then
            -- todo: trace message
            return head
        end
        local font = nil
        local char, id = isglyph(f)
        if char then
            font = id
        elseif id == hlist_code then
            font = getattr(f,a_runningtext)
        end
        if not font 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 wd, ht, dp    = getrangedimensions(parent,f,getnext(l))
        local method        = d.method
        local empty         = d.empty == v_yes
        local offset        = d.offset
        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_colormodel) 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,font) -- 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[font]) / 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,wd,ht,dp)
            if layer then
                setattr(r,a_viewerlayer,layer)
            end
            if empty then
                head = insertnodebefore(head,f,r)
                setlink(r,getnext(l))
                setprev(f)
                setnext(l)
                flushlist(f)
            else
                local k = new_kern(-wd)
                if foreground then
                    insertnodeafter(head,l,k)
                    insertnodeafter(head,k,r)
                    l = r
                else
                    head = insertnodebefore(head,f,r)
                    insertnodeafter(head,r,k)
                end
            end
            if trace_ruled then
                report_ruled("level %a, width %p, height %p, depth %p, nodes %a, text %a",
                    level,wd,ht,dp,n_tostring(f,l),n_tosequence(f,l,true))
            end
        end

        if mp and mp ~= "" then
            local r = usernutrule {
                width  = wd,
                height = ht,
                depth  = dp,
                type   = "mp",
                factor = e,
                offset = offset - (level-1)*dy, -- br ... different direction
                line   = rulethickness,
                data   = mp,
                ma     = colorspace,
                ca     = color,
                ta     = transparency,
            }
            inject(r,wd,ht,dp)
        else
            local tx = d.text
            if tx then
                local l = copylist(tx)
                if d["repeat"] == v_yes then
                    l = new_leader(wd,l)
                    setattrlist(l,tx)
                end
                l = hpack_nodes(l,wd,"exactly")
                inject(l,wd,ht,dp)
            else
                local hd = (offset+(level-1)*dy)*e - m
                local ht =  hd + rulethickness
                local dp = -hd + rulethickness
                inject(setcoloring(new_rule(wd,ht,dp),colorspace,color,transparency),wd,ht,dp)
            end
        end
        return head
    end

    rules.handler = function(head)
        local data = attributes.values[a_ruled]
        if data then
            head = processwords(a_ruled,data,flush_ruled,head)

        end
        return head
    end

    implement {
        name      = "setrule",
        actions   = rules.set,
        arguments = {
            {
                { "continue" },
                { "unit" },
                { "order" },
                { "level", "integer" },
                { "stack", "integer" },
                { "method", "integer" },
                { "offset", "number" },
                { "rulethickness" },
                { "dy", "number" },
                { "max", "number" },
                { "ma", "integer" },
                { "ca", "integer" },
                { "ta", "integer" },
                { "mp" },
                { "empty" },
                { "text", "box" },
                { "repeat" },
            }
        }
    }

end

do

    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')
    local enabled        = false

    local shifts         = nodes.shifts or { }
    nodes.shifts         = shifts

    function shifts.set(settings)
        if not enabled then
            -- we could disable when no more found
            enableaction("shipouts","nodes.shifts.handler")
            enabled = true
        end
        texsetattribute(a_shifted,registervalue(a_shifted,settings))
    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 = getrangedimensions(parent,first,next)
        local list = hpack_nodes(first,width,"exactly") -- we can use a simple pack
        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)])
        setshift(list,raise)
        setwhd(list,width,height,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

    shifts.handler = function(head)
        local data = attributes.values[a_shifted]
        if data then
            head = processwords(a_shifted,data,flush_shifted,head)
        end
        return head
    end

    implement {
        name      = "setshift",
        actions   = shifts.set,
        arguments = {
            {
                { "continue" },
                { "unit" },
                { "method", "integer" },
                { "dy", "number" },
            }
        }
    }

end

-- linefillers

do

    local linefillers = nodes.linefillers or { }
    nodes.linefillers = linefillers
    local enabled     = false

    local usernutrule = nuts.rules.userrule

    function linefillers.set(settings)
        if not enabled then
            enableaction("finalizers","nodes.linefillers.handler")
            enabled = true
        end
        texsetattribute(a_linefiller,registervalue(a_linefiller,settings))
    end

    local function linefiller(current,data,width,location)
        local height = data.height
        local depth  = data.depth
        local mp     = data.mp
        local ma     = data.ma
        local ca     = data.ca
        local ta     = data.ta
        if mp and mp ~= "" then
            return usernutrule {
                width     = width,
                height    = height,
                depth     = depth,
                type      = "mp",
                line      = data.rulethickness,
                data      = mp,
                ma        = ma,
                ca        = ca,
                ta        = ta,
                option    = location,
                direction = getdirection(current),
            }
        else
            return setcoloring(new_rule(width,height,depth),ma,ca,ta)
        end
    end

    function linefillers.filler(current,data,width,height,depth)
        if width and width > 0 then
            local height = height or data.height or 0
            local depth  = depth  or data.depth  or 0
            if (height + depth) ~= 0 then
                local mp = data.mp
                local ma = data.ma
                local ca = data.ca
                local ta = data.ta
                if mp and mp ~= "" then
                    return usernutrule {
                        width     = width,
                        height    = height,
                        depth     = depth,
                        type      = "mp",
                        line      = data.rulethickness,
                        data      = mp,
                        ma        = ma,
                        ca        = ca,
                        ta        = ta,
                        option    = location,
                        direction = getdirection(current),
                    }
                else
                    return setcoloring(new_rule(width,height,depth),ma,ca,ta)
                end
            end
        end
    end

    local function getskips(list) -- this could be a helper
        local ls = nil
        local rs = nil
        local is = nil
        local pl = nil
        local pr = nil
        local ok = false
        for n, subtype in nextglue, list do
            if subtype == rightskip_code then
                rs = n
            elseif subtype == parfillrightskip_code then
                pr = n
            elseif subtype == leftskip_code then
                ls = n
            elseif subtype == indentskip_code then
                is = n
            elseif subtype == parfillleftskip_code then
                pl = n
            end
        end
        return is, ls, pl, pr, rs
    end

    linefillers.handler = function(head)
        local data = attributes.values[a_linefiller]
        if data then
            -- we have a normalized line ..
            for current, id, subtype, list in nextlist, head do
                if subtype == linelist_code and list then
                    local a = getattr(current,a_linefiller)
                    if a then
                        local data = data[a]
                        if data then
                            local location   = data.location
                            local scope      = data.scope
                            local distance   = data.distance
                            local threshold  = data.threshold
                            local leftlocal  = false
                            local rightlocal = false
                            --
                            if scope == v_right then
                                leftlocal = true
                            elseif scope == v_left then
                                rightlocal = true
                            elseif scope == v_local then
                                leftlocal  = true
                                rightlocal = true
                            end
                            --
                            local is, ls, pl, pr, rs = getskips(list)
                            if ls and rs then
                                if location == v_left or location == v_both then
                                    local indentation = is and getwidth(is) or 0
                                    local leftfixed   = ls and getwidth(ls) or 0
                                    local lefttotal   = ls and effectiveglue(ls,current) or 0
                                    local width = lefttotal - (leftlocal and leftfixed or 0) + indentation - distance
                                    if width > threshold then
                                        if is then
                                            setwidth(is,0)
                                        end
                                        setglue(ls,leftlocal and getwidth(ls) or nil)
                                        if distance > 0 then
                                            insertnodeafter(list,ls,new_kern(distance))
                                        end
                                        insertnodeafter(list,ls,linefiller(current,data,width,"left"))
                                    end
                                end
                                --
                                if location == v_right or location == v_both then
                                    local rightfixed = rs and getwidth(rs) or 0
                                    local righttotal = rs and effectiveglue(rs,current) or 0
                                    local parfixed   = pr and getwidth(pr) or 0
                                    local partotal   = pr and effectiveglue(pr,current) or 0
                                    local width = righttotal - (rightlocal and rightfixed or 0) + partotal - distance
                                    if width > threshold then
                                        if pr then
                                            setglue(pr)
                                        end
                                        setglue(rs,rightlocal and getwidth(rs) or nil)
                                        if distance > 0 then
                                            insertnodebefore(list,rs,new_kern(distance))
                                        end
                                        insertnodebefore(list,rs,linefiller(current,data,width,"right"))
                                    end
                                end
                            else
                                -- error, not a properly normalized line
                            end
                        end
                    end
                end
            end
        end
        return head
    end

    implement {
        name      = "setlinefiller",
        actions   = linefillers.set,
        arguments = {
            {
                { "method", "integer" },
                { "location", "string" },
                { "scope", "string" },
                { "mp", "string" },
                { "ma", "integer" },
                { "ca", "integer" },
                { "ta", "integer" },
                { "depth", "dimension" },
                { "height", "dimension" },
                { "distance", "dimension" },
                { "threshold", "dimension" },
                { "rulethickness", "dimension" },
            }
        }
    }

end

-- We add a bonus feature here (experiment):

interfaces.implement {
    name      = "autorule",
    protected = true,
    public    = true,
    arguments = {
        {
            { "width", "dimension" },
            { "height", "dimension" },
            { "depth", "dimension" },
            { "xoffset", "dimension" },
            { "yoffset", "dimension" },
            { "left", "dimension" },
            { "right", "dimension" },
        },
    },
    actions   = function(t)
        local n = new_rule(
            t.width or runningrule,
            t.height or runningrule,
            t.depth or runningrule
        )
        setattrlist(n,true)
        setoffsets(n,t.xoffset,t.yoffset) -- ,t.left, t.right
        local l = t.left
        local r = t.right
        if l then
            setfield(n,"left",l)
        end
        if r then
            setfield(n,"right",r)
        end
        context(tonode(n))
    end
}