typo-dub.lua /size: 35 Kb    last modification: 2021-10-28 13:50
1if not modules then modules = { } end modules ['typo-dub'] = {
2    version   = 1.001,
3    comment   = "companion to typo-dir.mkiv",
4    author    = "Hans Hagen, PRAGMA-ADE, Hasselt NL",
5    copyright = "PRAGMA ADE / ConTeXt Development Team",
6    license   = "see context related readme files",
7    comment   = "Unicode bidi (sort of) variant b",
8}
9
10-- This is a follow up on typo-uba which itself is a follow up on t-bidi by Khaled Hosny which
11-- in turn is based on minibidi.c from Arabeyes. This is a further optimizations, as well as
12-- an update on some recent unicode bidi developments. There is (and will) also be more control
13-- added. As a consequence this module is somewhat slower than its precursor which itself is
14-- slower than the one-pass bidi handler. This is also a playground and I might add some plugin
15-- support.
16
17-- todo (cf html):
18--
19-- normal            The element does not offer a additional level of embedding with respect to the bidirectional algorithm. For inline elements implicit reordering works across element boundaries.
20-- embed             If the element is inline, this value opens an additional level of embedding with respect to the bidirectional algorithm. The direction of this embedding level is given by the direction property.
21-- bidi-override     For inline elements this creates an override. For block container elements this creates an override for inline-level descendants not within another block container element. This means that inside the element, reordering is strictly in sequence according to the direction property; the implicit part of the bidirectional algorithm is ignored.
22-- isolate           This keyword indicates that the element's container directionality should be calculated without considering the content of this element. The element is therefore isolated from its siblings. When applying its bidirectional-resolution algorithm, its container element treats it as one or several U+FFFC Object Replacement Character, i.e. like an image.
23-- isolate-override  This keyword applies the isolation behavior of the isolate keyword to the surrounding content and the override behavior o f the bidi-override keyword to the inner content.
24-- plaintext         This keyword makes the elements directionality calculated without considering its parent bidirectional state or the value of the direction property. The directionality is calculated using the P2 and P3 rules of the Unicode Bidirectional Algorithm.
25--                   This value allows to display data which has already formatted using a tool following the Unicode Bidirectional Algorithm.
26--
27-- todo: check for introduced errors
28-- todo: reuse list, we have size, so we can just change values (and auto allocate when not there)
29-- todo: reuse the stack
30-- todo: no need for a max check
31-- todo: collapse bound similar ranges (not ok yet)
32-- todo: combine some sweeps
33-- todo: removing is not needed when we inject at the same spot (only chnage the dir property)
34-- todo: isolated runs (isolating runs are similar to bidi=local in the basic analyzer)
35
36-- todo: check unicode addenda (from the draft):
37--
38-- Added support for canonical equivalents in BD16.
39-- Changed logic in N0 to not check forwards for context in the case of enclosed text opposite the embedding direction.
40-- Major extension of the algorithm to allow for the implementation of directional isolates and the introduction of new isolate-related values to the Bidi_Class property.
41-- Adds BD8, BD9, BD10, BD11, BD12, BD13, BD14, BD15, and BD16, Sections 2.4 and 2.5, and Rules X5a, X5b, X5c and X6a.
42-- Extensively revises Section 3.3.2, Explicit Levels and Directions and its existing X rules to formalize the algorithm for matching a PDF with the embedding or override initiator whose scope it terminates.
43-- Moves Rules X9 and X10 into a separate new Section 3.3.3, Preparations for Implicit Processing.
44-- Modifies Rule X10 to make the isolating run sequence the unit to which subsequent rules are applied.
45-- Modifies Rule W1 to change an NSM preceded by an isolate initiator or PDI into ON.
46-- Adds Rule N0 and makes other changes to Section 3.3.5, Resolving Neutral and Isolate Formatting Types to resolve bracket pairs to the same level.
47
48local insert, remove, unpack, concat = table.insert, table.remove, table.unpack, table.concat
49local utfchar = utf.char
50local setmetatable = setmetatable
51local formatters = string.formatters
52
53local directiondata       = characters.directions
54local mirrordata          = characters.mirrors
55local textclassdata       = characters.textclasses
56
57local nuts                = nodes.nuts
58
59local getnext             = nuts.getnext
60local getid               = nuts.getid
61local getsubtype          = nuts.getsubtype
62local getlist             = nuts.getlist
63local getchar             = nuts.getchar
64local getattr             = nuts.getattr
65local getprop             = nuts.getprop
66local getdirection        = nuts.getdirection
67
68local setprop             = nuts.setprop
69local setchar             = nuts.setchar
70local setdirection        = nuts.setdirection
71local setattrlist         = nuts.setattrlist
72
73local remove_node         = nuts.remove
74local insertnodeafter     = nuts.insertafter
75local insertnodebefore    = nuts.insertbefore
76
77local startofpar          = nuts.startofpar
78
79local nodepool            = nuts.pool
80local new_direction       = nodepool.direction
81
82local nodecodes           = nodes.nodecodes
83local gluecodes           = nodes.gluecodes
84
85local glyph_code          = nodecodes.glyph
86local glue_code           = nodecodes.glue
87local hlist_code          = nodecodes.hlist
88local vlist_code          = nodecodes.vlist
89local math_code           = nodecodes.math
90local dir_code            = nodecodes.dir
91local par_code            = nodecodes.par
92
93local parfillskip_code    = gluecodes.parfillskip
94
95local dirvalues           = nodes.dirvalues
96local lefttoright_code    = dirvalues.lefttoright
97local righttoleft_code    = dirvalues.righttoleft
98
99local maximum_stack       = 0xFF
100
101local a_directions        = attributes.private('directions')
102
103local directions          = typesetters.directions
104local setcolor            = directions.setcolor
105local getfences           = directions.getfences
106
107local remove_controls     = true  directives.register("typesetters.directions.removecontrols",function(v) remove_controls  = v end)
108----- analyze_fences      = true  directives.register("typesetters.directions.analyzefences", function(v) analyze_fences   = v end)
109
110local report_directions   = logs.reporter("typesetting","directions three")
111
112local trace_directions    = false trackers.register("typesetters.directions",         function(v) trace_directions = v end)
113local trace_details       = false trackers.register("typesetters.directions.details", function(v) trace_details    = v end)
114local trace_list          = false trackers.register("typesetters.directions.list",    function(v) trace_list       = v end)
115
116-- strong (old):
117--
118-- l   : left to right
119-- r   : right to left
120-- lro : left to right override
121-- rlo : left to left override
122-- lre : left to right embedding
123-- rle : left to left embedding
124-- al  : right to legt arabic (esp punctuation issues)
125--
126-- weak:
127--
128-- en  : english number
129-- es  : english number separator
130-- et  : english number terminator
131-- an  : arabic number
132-- cs  : common number separator
133-- nsm : nonspacing mark
134-- bn  : boundary neutral
135--
136-- neutral:
137--
138-- b  : paragraph separator
139-- s  : segment separator
140-- ws : whitespace
141-- on : other neutrals
142--
143-- interesting: this is indeed better (and more what we expect i.e. we already use this split
144-- in the old original (also these isolates)
145--
146-- strong (new):
147--
148-- l   : left to right
149-- r   : right to left
150-- al  : right to left arabic (esp punctuation issues)
151--
152-- explicit: (new)
153--
154-- lro : left to right override
155-- rlo : left to left override
156-- lre : left to right embedding
157-- rle : left to left embedding
158-- pdf : pop dir format
159-- lri : left to right isolate
160-- rli : left to left isolate
161-- fsi : first string isolate
162-- pdi : pop directional isolate
163
164local whitespace = {
165    lre = true,
166    rle = true,
167    lro = true,
168    rlo = true,
169    pdf = true,
170    bn  = true,
171    ws  = true,
172}
173
174local b_s_ws_on = {
175    b   = true,
176    s   = true,
177    ws  = true,
178    on  = true
179}
180
181-- tracing
182
183local function show_list(list,size,what)
184    local what   = what or "direction"
185    local joiner = utfchar(0x200C)
186    local result = { }
187    for i=1,size do
188        local entry     = list[i]
189        local character = entry.char
190        local direction = entry[what]
191        if character == 0xFFFC then
192            local first = entry.id
193            local last  = entry.last
194            local skip  = entry.skip
195            if last then
196                result[i] = formatters["%-3s:%s %s..%s (%i)"](direction,joiner,nodecodes[first],nodecodes[last],skip or 0)
197            else
198                result[i] = formatters["%-3s:%s %s (%i)"](direction,joiner,nodecodes[first],skip or 0)
199            end
200        elseif character >= 0x202A and character <= 0x202C then
201            result[i] = formatters["%-3s:%s %U"](direction,joiner,character)
202        else
203            result[i] = formatters["%-3s:%s %c %U"](direction,joiner,character,character)
204        end
205    end
206    return concat(result,joiner .. " | " .. joiner)
207end
208
209-- preparation
210
211local function show_done(list,size)
212    local joiner = utfchar(0x200C)
213    local result = { }
214    local format = formatters["<%s>"]
215    for i=1,size do
216        local entry     = list[i]
217        local character = entry.char
218        local begindir  = entry.begindir
219        local enddir    = entry.enddir
220        if begindir then
221            result[#result+1] = format(begindir)
222        end
223        if entry.remove then
224            -- continue
225        elseif character == 0xFFFC then
226            result[#result+1] = format("?")
227        elseif character == 0x0020 then
228            result[#result+1] = format(" ")
229        elseif character >= 0x202A and character <= 0x202C then
230            result[#result+1] = format(entry.original)
231        else
232            result[#result+1] = utfchar(character)
233        end
234        if enddir then
235            result[#result+1] = format(enddir)
236        end
237    end
238    return concat(result,joiner)
239end
240
241-- keeping the list and overwriting doesn't save much runtime, only a few percent
242-- char is only used for mirror, so in fact we can as well only store it for
243-- glyphs only
244
245local function build_list(head)
246    -- P1
247    local current = head
248    local list    = { }
249    local size    = 0
250    while current do
251        size = size + 1
252        local id = getid(current)
253        if getprop(current,"directions") then
254            local skip = 0
255            local last = id
256            current    = getnext(current)
257            while current do
258                local id = getid(current)
259                if getprop(current,"directions") then
260                    skip    = skip + 1
261                    last    = id
262                    current = getnext(current)
263                else
264                    break
265                end
266            end
267            if id == last then -- the start id
268                list[size] = { char = 0xFFFC, direction = "on", original = "on", level = 0, skip = skip, id = id }
269            else
270                list[size] = { char = 0xFFFC, direction = "on", original = "on", level = 0, skip = skip, id = id, last = last }
271            end
272        elseif id == glyph_code then
273            local chr = getchar(current)
274            local dir = directiondata[chr]
275            list[size] = { char = chr, direction = dir, original = dir, level = 0 }
276            current = getnext(current)
277        elseif id == glue_code then -- and how about kern
278            list[size] = { char = 0x0020, direction = "ws", original = "ws", level = 0 }
279            current = getnext(current)
280        elseif id == dir_code then
281            local direction, pop = getdirection(current)
282            if direction == lefttoright_code then
283                if pop then
284                    list[size] = { char = 0x202C, direction = "pdf", original = "pdf", level = 0 }
285                else
286                    list[size] = { char = 0x202A, direction = "lre", original = "lre", level = 0 }
287                end
288            elseif direction == righttoleft_code then
289                if pop then
290                    list[size] = { char = 0x202C, direction = "pdf", original = "pdf", level = 0 }
291                else
292                    list[size] = { char = 0x202B, direction = "rle", original = "rle", level = 0 }
293                end
294            else
295                list[size] = { char = 0xFFFC, direction = "on", original = "on", level = 0, id = id } -- object replacement character
296            end
297            current = getnext(current)
298        elseif id == math_code then
299            local skip = 0
300            current    = getnext(current)
301            while getid(current) ~= math_code do
302                skip    = skip + 1
303                current = getnext(current)
304            end
305            skip       = skip + 1
306            current    = getnext(current)
307            list[size] = { char = 0xFFFC, direction = "on", original = "on", level = 0, skip = skip, id = id }
308        else
309            local skip = 0
310            local last = id
311            current    = getnext(current)
312            while n do
313                local id = getid(current)
314                if id ~= glyph_code and id ~= glue_code and id ~= dir_code then
315                    skip    = skip + 1
316                    last    = id
317                    current = getnext(current)
318                else
319                    break
320                end
321            end
322            if id == last then -- the start id
323                list[size] = { char = 0xFFFC, direction = "on", original = "on", level = 0, skip = skip, id = id }
324            else
325                list[size] = { char = 0xFFFC, direction = "on", original = "on", level = 0, skip = skip, id = id, last = last }
326            end
327        end
328    end
329    return list, size
330end
331
332-- new
333
334-- we could support ( ] and [ ) and such ...
335
336-- ש ) ל ( א       0-0
337-- ש ( ל ] א       0-0
338-- ש ( ל ) א       2-4
339-- ש ( ל [ א ) כ ] 2-6
340-- ש ( ל ] א ) כ   2-6
341-- ש ( ל ) א ) כ   2-4
342-- ש ( ל ( א ) כ   4-6
343-- ש ( ל ( א ) כ ) 2-8,4-6
344-- ש ( ל [ א ] כ ) 2-8,4-6
345
346local function resolve_fences(list,size,start,limit)
347    -- N0: funny effects, not always better, so it's an option
348    local stack    = { }
349    local nofstack = 0
350    for i=start,limit do
351        local entry = list[i]
352        if entry.direction == "on" then
353            local char   = entry.char
354            local mirror = mirrordata[char]
355            if mirror then
356                local class = textclassdata[char]
357                entry.mirror = mirror
358                entry.class  = class
359                if class == "open" then
360                    nofstack        = nofstack + 1
361                    stack[nofstack] = { mirror, i, false }
362                elseif nofstack == 0 then
363                    -- skip
364                elseif class == "close" then
365                    while nofstack > 0 do
366                        local stacktop = stack[nofstack]
367                        if stacktop[1] == char then
368                            local open  = stacktop[2]
369                            local close = i
370                            list[open ].paired = close
371                            list[close].paired = open
372                            break
373                        else
374                            -- do we mirror or not
375                        end
376                        nofstack = nofstack - 1
377                    end
378                end
379            end
380        end
381    end
382end
383
384-- local function test_fences(str)
385--     local list  = { }
386--     for s in string.gmatch(str,".") do
387--         local b = utf.byte(s)
388--         list[#list+1] = { c = s, char = b, direction = directiondata[b] }
389--     end
390--     resolve_fences(list,#list,1,#size)
391--     inspect(list)
392-- end
393--
394-- test_fences("a(b)c(d)e(f(g)h)i")
395-- test_fences("a(b[c)d]")
396
397-- the action
398
399local function get_baselevel(head,list,size,direction)
400    if direction == lefttoright_code or direction == righttoleft_code then
401        return direction, true
402    elseif getid(head) == par_code and startofpar(head) then
403        direction = getdirection(head)
404        if direction == lefttoright_code or direction == righttoleft_code then
405            return direction, true
406        end
407    end
408    -- P2, P3
409    for i=1,size do
410        local entry     = list[i]
411        local direction = entry.direction
412        if direction == "r" or direction == "al" then -- and an ?
413            return righttoleft_code, true
414        elseif direction == "l" then
415            return lefttoright_code, true
416        end
417    end
418    return lefttoright_code, false
419end
420
421local function resolve_explicit(list,size,baselevel)
422    -- X1
423    local level    = baselevel
424    local override = "on"
425    local stack    = { }
426    local nofstack = 0
427    for i=1,size do
428        local entry     = list[i]
429        local direction = entry.direction
430        -- X2
431        if direction == "rle" then
432            if nofstack < maximum_stack then
433                nofstack        = nofstack + 1
434                stack[nofstack] = { level, override }
435                level           = level + (level % 2 == 1 and 2 or 1) -- least_greater_odd(level)
436                override        = "on"
437                entry.level     = level
438                entry.direction = "bn"
439                entry.remove    = true
440            elseif trace_directions then
441                report_directions("stack overflow at position %a with direction %a",i,direction)
442            end
443        -- X3
444        elseif direction == "lre" then
445            if nofstack < maximum_stack then
446                nofstack        = nofstack + 1
447                stack[nofstack] = { level, override }
448                level           = level + (level % 2 == 1 and 1 or 2) -- least_greater_even(level)
449                override        = "on"
450                entry.level     = level
451                entry.direction = "bn"
452                entry.remove    = true
453            elseif trace_directions then
454                report_directions("stack overflow at position %a with direction %a",i,direction)
455            end
456        -- X4
457        elseif direction == "rlo" then
458            if nofstack < maximum_stack then
459                nofstack        = nofstack + 1
460                stack[nofstack] = { level, override }
461                level           = level + (level % 2 == 1 and 2 or 1) -- least_greater_odd(level)
462                override        = "r"
463                entry.level     = level
464                entry.direction = "bn"
465                entry.remove    = true
466            elseif trace_directions then
467                report_directions("stack overflow at position %a with direction %a",i,direction)
468            end
469        -- X5
470        elseif direction == "lro" then
471            if nofstack < maximum_stack then
472                nofstack        = nofstack + 1
473                stack[nofstack] = { level, override }
474                level           = level + (level % 2 == 1 and 1 or 2) -- least_greater_even(level)
475                override        = "l"
476                entry.level     = level
477                entry.direction = "bn"
478                entry.remove    = true
479            elseif trace_directions then
480                report_directions("stack overflow at position %a with direction %a",i,direction)
481            end
482        -- X7
483        elseif direction == "pdf" then
484            if nofstack > 0 then
485                local stacktop  = stack[nofstack]
486                level           = stacktop[1]
487                override        = stacktop[2]
488                nofstack        = nofstack - 1
489                entry.level     = level
490                entry.direction = "bn"
491                entry.remove    = true
492            elseif trace_directions then
493                report_directions("stack underflow at position %a with direction %a",
494                    i, direction)
495            end
496        -- X6
497        else
498            entry.level = level
499            if override ~= "on" then
500                entry.direction = override
501            end
502        end
503    end
504    -- X8 (reset states and overrides after paragraph)
505end
506
507local function resolve_weak(list,size,start,limit,orderbefore,orderafter)
508    -- W1: non spacing marks get the direction of the previous character
509    for i=start,limit do
510        local entry = list[i]
511        if entry.direction == "nsm" then
512            if i == start then
513                entry.direction = orderbefore
514            else
515                entry.direction = list[i-1].direction
516            end
517        end
518    end
519    -- W2: mess with numbers and arabic
520    for i=start,limit do
521        local entry = list[i]
522        if entry.direction == "en" then
523            for j=i-1,start,-1 do
524                local prev = list[j]
525                local direction = prev.direction
526                if direction == "al" then
527                    entry.direction = "an"
528                    break
529                elseif direction == "r" or direction == "l" then
530                    break
531                end
532            end
533        end
534    end
535    -- W3
536    for i=start,limit do
537        local entry = list[i]
538        if entry.direction == "al" then
539            entry.direction = "r"
540        end
541    end
542    -- W4: make separators number
543    if true then
544        for i=start+1,limit-1 do
545            local entry     = list[i]
546            local direction = entry.direction
547            if direction == "es" then
548                if list[i-1].direction == "en" and list[i+1].direction == "en" then
549                    entry.direction = "en"
550                end
551            elseif direction == "cs" then
552                local prevdirection = list[i-1].direction
553                if prevdirection == "en" then
554                    if list[i+1].direction == "en" then
555                        entry.direction = "en"
556                    end
557                elseif prevdirection == "an" and list[i+1].direction == "an" then
558                    entry.direction = "an"
559                end
560            end
561        end
562    else -- probably more efficient
563        local runner = start + 2
564        if runner <= limit then
565            local before = list[start]
566            local entry  = list[start + 1]
567            local after  = list[runner]
568            while after do
569                local direction = entry.direction
570                if direction == "es" then
571                    if before.direction == "en" and after.direction == "en" then
572                        entry.direction = "en"
573                    end
574                elseif direction == "cs" then
575                    local prevdirection = before.direction
576                    if prevdirection == "en" then
577                        if after.direction == "en" then
578                            entry.direction = "en"
579                        end
580                    elseif prevdirection == "an" and after.direction == "an" then
581                        entry.direction = "an"
582                    end
583                end
584                before  = current
585                current = after
586                after   = list[runner]
587            end
588        end
589    end
590    -- W5
591    local i = start
592    while i <= limit do
593        if list[i].direction == "et" then
594            local runstart = i
595            local runlimit = runstart
596            for i=runstart,limit do
597                if list[i].direction == "et" then
598                    runlimit = i
599                else
600                    break
601                end
602            end
603            local rundirection = runstart == start and sor or list[runstart-1].direction
604            if rundirection ~= "en" then
605                rundirection = runlimit == limit and orderafter or list[runlimit+1].direction
606            end
607            if rundirection == "en" then
608                for j=runstart,runlimit do
609                    list[j].direction = "en"
610                end
611            end
612            i = runlimit
613        end
614        i = i + 1
615    end
616    -- W6
617    for i=start,limit do
618        local entry     = list[i]
619        local direction = entry.direction
620        if direction == "es" or direction == "et" or direction == "cs" then
621            entry.direction = "on"
622        end
623    end
624    -- W7
625    for i=start,limit do
626        local entry = list[i]
627        if entry.direction == "en" then
628            local prev_strong = orderbefore
629            for j=i-1,start,-1 do
630                local direction = list[j].direction
631                if direction == "l" or direction == "r" then
632                    prev_strong = direction
633                    break
634                end
635            end
636            if prev_strong == "l" then
637                entry.direction = "l"
638            end
639        end
640    end
641end
642
643local function resolve_neutral(list,size,start,limit,orderbefore,orderafter)
644    -- N1, N2
645    for i=start,limit do
646        local entry = list[i]
647        if b_s_ws_on[entry.direction] then
648            -- this needs checking
649            local leading_direction, trailing_direction, resolved_direction
650            local runstart = i
651            local runlimit = runstart
652--             for j=runstart,limit do
653            for j=runstart+1,limit do
654                if b_s_ws_on[list[j].direction] then
655--                     runstart = j
656                    runlimit = j
657                else
658                    break
659                end
660            end
661            if runstart == start then
662                leading_direction = orderbefore
663            else
664                leading_direction = list[runstart-1].direction
665                if leading_direction == "en" or leading_direction == "an" then
666                    leading_direction = "r"
667                end
668            end
669            if runlimit == limit then
670                trailing_direction = orderafter
671            else
672                trailing_direction = list[runlimit+1].direction
673                if trailing_direction == "en" or trailing_direction == "an" then
674                    trailing_direction = "r"
675                end
676            end
677            if leading_direction == trailing_direction then
678                -- N1
679                resolved_direction = leading_direction
680            else
681                -- N2 / does the weird period
682                resolved_direction = entry.level % 2 == 1 and "r" or "l"
683            end
684            for j=runstart,runlimit do
685                list[j].direction = resolved_direction
686            end
687            i = runlimit
688        end
689        i = i + 1
690    end
691end
692
693local function resolve_implicit(list,size,start,limit,orderbefore,orderafter,baselevel)
694    for i=start,limit do
695        local entry     = list[i]
696        local level     = entry.level
697        local direction = entry.direction
698        if level % 2 ~= 1 then -- even
699            -- I1
700            if direction == "r" then
701                entry.level = level + 1
702            elseif direction == "an" or direction == "en" then
703                entry.level = level + 2
704            end
705        else
706            -- I2
707            if direction == "l" or direction == "en" or direction == "an" then
708                entry.level = level + 1
709            end
710        end
711    end
712end
713
714local function resolve_levels(list,size,baselevel,analyze_fences)
715    -- X10
716    local start = 1
717    while start < size do
718        local level = list[start].level
719        local limit = start + 1
720        while limit < size and list[limit].level == level do
721            limit = limit + 1
722        end
723        local prev_level  = start == 1    and baselevel or list[start-1].level
724        local next_level  = limit == size and baselevel or list[limit+1].level
725        local orderbefore = (level > prev_level and level or prev_level) % 2 == 1 and "r" or "l"
726        local orderafter  = (level > next_level and level or next_level) % 2 == 1 and "r" or "l"
727        -- W1 .. W7
728        resolve_weak(list,size,start,limit,orderbefore,orderafter)
729        -- N0
730        if analyze_fences then
731            resolve_fences(list,size,start,limit)
732        end
733        -- N1 .. N2
734        resolve_neutral(list,size,start,limit,orderbefore,orderafter)
735        -- I1 .. I2
736        resolve_implicit(list,size,start,limit,orderbefore,orderafter,baselevel)
737        start = limit
738    end
739    -- L1
740    for i=1,size do
741        local entry     = list[i]
742        local direction = entry.original
743        -- (1)
744        if direction == "s" or direction == "b" then
745            entry.level = baselevel
746            -- (2)
747            for j=i-1,1,-1 do
748                local entry = list[j]
749                if whitespace[entry.original] then
750                    entry.level = baselevel
751                else
752                    break
753                end
754            end
755        end
756    end
757    -- (3)
758    for i=size,1,-1 do
759        local entry = list[i]
760        if whitespace[entry.original] then
761            entry.level = baselevel
762        else
763            break
764        end
765    end
766    -- L4
767    if analyze_fences then
768        for i=1,size do
769            local entry = list[i]
770            if entry.level % 2 == 1 then -- odd(entry.level)
771                if entry.mirror and not entry.paired then
772                    entry.mirror = false
773                end
774                -- okay
775            elseif entry.mirror then
776                entry.mirror = false
777            end
778        end
779    else
780        for i=1,size do
781            local entry = list[i]
782            if entry.level % 2 == 1 then -- odd(entry.level)
783                local mirror = mirrordata[entry.char]
784                if mirror then
785                    entry.mirror = mirror
786                end
787            end
788        end
789    end
790end
791
792local function insert_dir_points(list,size)
793    -- L2, but no actual reversion is done, we simply annotate where
794    -- begindir/endddir node will be inserted.
795    local maxlevel = 0
796    local toggle   = true
797    for i=1,size do
798        local level = list[i].level
799        if level > maxlevel then
800            maxlevel = level
801        end
802    end
803    for level=0,maxlevel do
804        local started  -- = false
805        local begindir -- = nil
806        local enddir   -- = nil
807        local prev     -- = nil
808        if toggle then
809            begindir = lefttoright_code
810            enddir   = lefttoright_code
811            toggle   = false
812        else
813            begindir = righttoleft_code
814            enddir   = righttoleft_code
815            toggle   = true
816        end
817        for i=1,size do
818            local entry = list[i]
819            if entry.level >= level then
820                if not started then
821                    entry.begindir = begindir
822                    started        = true
823                end
824            else
825                if started then
826                    prev.enddir = enddir
827                    started     = false
828                end
829            end
830            prev = entry
831        end
832    end
833    -- make sure to close the run at end of line
834    local last = list[size]
835    if not last.enddir then
836        local s = { }
837        local n = 0
838        for i=1,size do
839            local entry = list[i]
840            local e = entry.enddir
841            local b = entry.begindir
842            if e then
843                n = n - 1
844            end
845            if b then
846                n = n + 1
847                s[n] = b
848            end
849        end
850        if n > 0 then
851            if trace_list and n > 1 then
852                report_directions("unbalanced list")
853            end
854            last.enddir = s[n] == righttoleft_code and righttoleft_code or lefttoright_code
855        end
856    end
857end
858
859local function apply_to_list(list,size,head,pardir)
860    local index   = 1
861    local current = head
862    if trace_list then
863        report_directions("start run")
864    end
865    while current do
866        if index > size then
867            report_directions("fatal error, size mismatch")
868            break
869        end
870        local id       = getid(current)
871        local entry    = list[index]
872        local begindir = entry.begindir
873        local enddir   = entry.enddir
874        setprop(current,"directions",true)
875        if id == glyph_code then
876            local mirror = entry.mirror
877            if mirror then
878                setchar(current,mirror)
879            end
880            if trace_directions then
881                local direction = entry.direction
882                if trace_list then
883                    local original = entry.original
884                    local char     = entry.char
885                    local level    = entry.level
886                    if direction == original then
887                        report_directions("%2i : %C : %s",level,char,direction)
888                    else
889                        report_directions("%2i : %C : %s -> %s",level,char,original,direction)
890                    end
891                end
892                setcolor(current,direction,false,mirror)
893            end
894        elseif id == hlist_code or id == vlist_code then
895            setdirection(current,pardir) -- is this really needed?
896        elseif id == glue_code then
897            if enddir and getsubtype(current) == parfillskip_code then
898                -- insert the last enddir before \parfillskip glue
899                local d = new_direction(enddir,true)
900             -- setprop(d,"directions",true)
901             -- setattrlist(d,current)
902                head = insertnodebefore(head,current,d)
903                enddir = false
904            end
905        elseif begindir then
906            if id == par_code and startofpar(current) then
907                -- par should always be the 1st node
908                local d = new_direction(begindir)
909             -- setprop(d,"directions",true)
910             -- setattrlist(d,current)
911                head, current = insertnodeafter(head,current,d)
912                begindir = nil
913            end
914        end
915        if begindir then
916            local d = new_direction(begindir)
917         -- setprop(d,"directions",true)
918         -- setattrlist(d,current)
919            head = insertnodebefore(head,current,d)
920        end
921        local skip = entry.skip
922        if skip and skip > 0 then
923            for i=1,skip do
924                current = getnext(current)
925                setprop(current,"directions",true)
926            end
927        end
928        if enddir then
929            local d = new_direction(enddir,true)
930         -- setprop(d,"directions",true)
931         -- setattrlist(d,current)
932            head, current = insertnodeafter(head,current,d)
933        end
934        if not entry.remove then
935            current = getnext(current)
936        elseif remove_controls then
937            -- X9
938            head, current = remove_node(head,current,true)
939        else
940            current = getnext(current)
941        end
942        index = index + 1
943    end
944    if trace_list then
945        report_directions("stop run")
946    end
947    return head
948end
949
950local function process(head,direction,only_one,where)
951    -- for the moment a whole paragraph property
952    local attr = getattr(head,a_directions)
953    local analyze_fences = getfences(attr)
954    --
955    local list, size = build_list(head)
956    local baselevel, dirfound = get_baselevel(head,list,size,direction)
957    if not dirfound and trace_details then
958        report_directions("no initial direction found, gambling")
959    end
960    if trace_details then
961        report_directions("before : %s",show_list(list,size,"original"))
962    end
963    resolve_explicit(list,size,baselevel)
964    resolve_levels(list,size,baselevel,analyze_fences)
965    insert_dir_points(list,size)
966    if trace_details then
967        report_directions("after  : %s",show_list(list,size,"direction"))
968        report_directions("result : %s",show_done(list,size))
969    end
970    return apply_to_list(list,size,head,baselevel)
971end
972
973directions.installhandler(interfaces.variables.two,process)
974