• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sile-typesetter / sile / 9428435077

08 Jun 2024 11:35AM UTC coverage: 64.56% (-9.9%) from 74.46%
9428435077

push

github

web-flow
Merge pull request #2047 from alerque/end-pars

23 of 46 new or added lines in 5 files covered. (50.0%)

1684 existing lines in 60 files now uncovered.

11145 of 17263 relevant lines covered (64.56%)

4562.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.54
/packages/bidi/init.lua
1
local base = require("packages.base")
60✔
2

3
local package = pl.class(base)
60✔
4
package._name = "bidi"
60✔
5

6
local icu = require("justenoughicu")
60✔
7

8
local function reverse_portion (tbl, s, e)
9
   local rv = {}
13✔
10
   for i = 1, s - 1 do
129✔
11
      rv[#rv + 1] = tbl[i]
116✔
12
   end
13
   for i = e, s, -1 do
32✔
14
      rv[#rv + 1] = tbl[i]
19✔
15
   end
16
   for i = e + 1, #tbl do
148✔
17
      rv[#rv + 1] = tbl[i]
135✔
18
   end
19
   return rv
13✔
20
end
21

22
local function create_matrix (line, base_level)
23
   -- L2; create a transformation matrix of elements
24
   -- such that output[matrix[i]] = input[i]
25
   -- e.g. No reversions required: [1, 2, 3, 4, 5]
26
   -- Levels [0, 0, 0, 1, 1] -> [1, 2, 3, 5, 4]
27

28
   local max_level = 0
562✔
29
   local matrix = {}
562✔
30
   for i, c in next, line do
13,429✔
31
      if c.level > max_level then
12,867✔
32
         max_level = c.level
11✔
33
      end
34
      matrix[i] = i
12,867✔
35
   end
36

37
   for level = base_level + 1, max_level do
573✔
38
      local level_start
39
      for i, _ in next, line do
228✔
40
         if line[i].level >= level then
217✔
41
            if not level_start then
19✔
42
               level_start = i
13✔
43
            elseif i == #line then
6✔
44
               local level_end = i
×
45
               matrix = reverse_portion(matrix, level_start, level_end)
×
46
               level_start = nil
×
47
            end
48
         else
49
            if level_start then
198✔
50
               local level_end = i - 1
13✔
51
               matrix = reverse_portion(matrix, level_start, level_end)
26✔
52
               level_start = nil
13✔
53
            end
54
         end
55
      end
56
   end
57

58
   return matrix
562✔
59
end
60

61
local function reverse_each_node (nodelist)
62
   for j = 1, #nodelist do
192✔
63
      if nodelist[j].type == "hbox" then
96✔
64
         if nodelist[j].value.items then
96✔
65
            SU.flip_in_place(nodelist[j].value.items)
96✔
66
         end
67
         SU.flip_in_place(nodelist[j].value.glyphString)
96✔
68
      end
69
   end
70
end
71

72
local nodeListToText = function (nl)
73
   local owners, text = {}, {}
333✔
74
   local p = 1
333✔
75
   for i = 1, #nl do
1,818✔
76
      local n = nl[i]
1,485✔
77
      if n.text then
1,485✔
78
         local utfchars = SU.splitUtf8(n.text)
494✔
79
         for j = 1, #utfchars do
19,320✔
80
            owners[p] = { node = n, pos = j }
18,826✔
81
            text[p] = utfchars[j]
18,826✔
82
            p = p + 1
18,826✔
83
         end
84
      else
85
         owners[p] = { node = n }
991✔
86
         text[p] = luautf8.char(0xFFFC)
991✔
87
         p = p + 1
991✔
88
      end
89
   end
90
   return owners, text
333✔
91
end
92

93
local splitNodeAtPos = function (n, splitstart, p)
94
   if n.is_unshaped then
23✔
95
      local utf8chars = SU.splitUtf8(n.text)
23✔
96
      local n2 = SILE.types.node.unshaped({ text = "", options = pl.tablex.copy(n.options) })
46✔
97
      local n1 = SILE.types.node.unshaped({ text = "", options = pl.tablex.copy(n.options) })
46✔
98
      for i = splitstart, #utf8chars do
3,715✔
99
         if i <= p then
3,692✔
100
            n1.text = n1.text .. utf8chars[i]
553✔
101
         else
102
            n2.text = n2.text .. utf8chars[i]
3,139✔
103
         end
104
      end
105
      return n1, n2
23✔
106
   else
107
      SU.error("Unsure how to split node " .. tostring(n) .. " at position " .. p, true)
×
108
   end
109
end
110

111
local splitNodelistIntoBidiRuns = function (typesetter)
112
   local nl = typesetter.state.nodes
333✔
113
   if #nl == 0 then
333✔
114
      return nl
×
115
   end
116
   local owners, text = nodeListToText(nl)
333✔
117
   local base_level = typesetter.frame:writingDirection() == "RTL" and 1 or 0
666✔
118
   local runs = { icu.bidi_runs(table.concat(text), typesetter.frame:writingDirection()) }
666✔
119
   table.sort(runs, function (a, b)
666✔
120
      return a.start < b.start
69✔
121
   end)
122
   -- local newNl = {}
123
   -- Split nodes on run boundaries
124
   for i = 1, #runs do
692✔
125
      local run = runs[i]
359✔
126
      local thisOwner = owners[run.start + run.length]
359✔
127
      local nextOwner = owners[run.start + 1 + run.length]
359✔
128
      -- print(thisOwner, nextOwner)
129
      if nextOwner and thisOwner.node == nextOwner.node then
359✔
130
         local before, after = splitNodeAtPos(nextOwner.node, 1, nextOwner.pos - 1)
23✔
131
         -- print(before, after)
132
         local start = nil
23✔
133
         for j = run.start + 1, run.start + run.length do
582✔
134
            if owners[j].node == nextOwner.node then
559✔
135
               if not start then
553✔
136
                  start = j
23✔
137
               end
138
               owners[j] = { node = before, pos = j - start + 1 }
553✔
139
            end
140
         end
141
         for j = run.start + 1 + run.length, #owners do
3,162✔
142
            if owners[j].node == nextOwner.node then
3,139✔
143
               owners[j] = { node = after, pos = j - (run.start + run.length) }
3,139✔
144
            end
145
         end
146
      end
147
   end
148
   -- Assign direction/level to nodes
149
   for i = 1, #runs do
692✔
150
      local runstart = runs[i].start + 1
359✔
151
      local runend = runstart + runs[i].length - 1
359✔
152
      for j = runstart, runend do
20,176✔
153
         if owners[j].node and owners[j].node.options then
19,817✔
154
            owners[j].node.options.direction = runs[i].dir
18,826✔
155
            owners[j].node.options.bidilevel = runs[i].level - base_level
18,826✔
156
         end
157
      end
158
   end
159
   -- String together nodelist
160
   nl = {}
333✔
161
   for i = 1, #owners do
20,150✔
162
      if #nl and nl[#nl] ~= owners[i].node then
19,817✔
163
         nl[#nl + 1] = owners[i].node
1,508✔
164
         -- print(nl[#nl], nl[#nl].options)
165
      end
166
   end
167
   -- for i = 1, #nl do print(i, nl[i]) end
168
   return nl
333✔
169
end
170

171
local bidiBoxupNodes = function (typesetter)
172
   local allDone = true
1,134✔
173
   for i = 1, #typesetter.state.nodes do
2,619✔
174
      if not typesetter.state.nodes[i].bidiDone then
1,879✔
175
         allDone = false
1,277✔
176
      end
177
   end
178
   if allDone then
1,134✔
179
      return typesetter:nobidi_boxUpNodes()
801✔
180
   end
181
   local newNodeList = splitNodelistIntoBidiRuns(typesetter)
333✔
182
   typesetter:shapeAllNodes(newNodeList)
333✔
183
   typesetter.state.nodes = newNodeList
333✔
184
   local vboxlist = typesetter:nobidi_boxUpNodes()
333✔
185
   -- Scan for out-of-direction material
186
   for i = 1, #vboxlist do
1,594✔
187
      local v = vboxlist[i]
1,261✔
188
      if v.is_vbox then
1,261✔
189
         package.reorder(nil, v, typesetter)
562✔
190
      end
191
   end
192
   return vboxlist
333✔
193
end
194

195
function package.reorder (_, n, typesetter)
60✔
196
   local nl = n.nodes
562✔
197
   -- local newNl = {}
198
   -- local matrix = {}
199
   local levels = {}
562✔
200
   local base_level = typesetter.frame:writingDirection() == "RTL" and 1 or 0
1,124✔
201
   for i = 1, #nl do
13,429✔
202
      if nl[i].options and nl[i].options.bidilevel then
12,867✔
203
         levels[i] = { level = nl[i].options.bidilevel }
4,813✔
204
      end
205
   end
206
   for i = 1, #nl do
13,429✔
207
      if not levels[i] then
12,867✔
208
         -- resolve neutrals
209
         local left_level, right_level
210
         for left = i - 1, 1, -1 do
15,329✔
211
            if nl[left].options and nl[left].options.bidilevel then
13,095✔
212
               left_level = nl[left].options.bidilevel
5,820✔
213
               break
5,820✔
214
            end
215
         end
216
         for right = i + 1, #nl do
15,329✔
217
            if nl[right].options and nl[right].options.bidilevel then
12,904✔
218
               right_level = nl[right].options.bidilevel
5,629✔
219
               break
5,629✔
220
            end
221
         end
222
         levels[i] = { level = (left_level == right_level and left_level or 0) }
8,054✔
223
      end
224
   end
225
   local matrix = create_matrix(levels, 0)
562✔
226
   local rv = {}
562✔
227
   -- for i = 1, #nl do print(i, nl[i], levels[i]) end
228
   for i = 1, #nl do
13,429✔
229
      if nl[i].is_nnode and levels[i].level % 2 ~= base_level then
12,867✔
230
         SU.flip_in_place(nl[i].nodes)
96✔
231
         reverse_each_node(nl[i].nodes)
192✔
232
      elseif nl[i].is_discretionary and levels[i].level % 2 ~= base_level and not nl[i].bidiDone then
12,771✔
233
         for j = 1, #nl[i].replacement do
×
234
            if nl[i].replacement[j].is_nnode then
×
235
               SU.flip_in_place(nl[i].replacement[j].nodes)
×
236
               reverse_each_node(nl[i].replacement[j].nodes)
×
237
            end
238
         end
239
         for j = 1, #nl[i].prebreak do
×
240
            if nl[i].prebreak[j].is_nnode then
×
241
               SU.flip_in_place(nl[i].prebreak[j].nodes)
×
242
               reverse_each_node(nl[i].prebreak[j].nodes)
×
243
            end
244
         end
245
         for j = 1, #nl[i].postbreak do
×
246
            if nl[i].postbreak[j].is_nnode then
×
247
               SU.flip_in_place(nl[i].postbreak[j].nodes)
×
248
               reverse_each_node(nl[i].postbreak[j].nodes)
×
249
            end
250
         end
251
      end
252
      rv[matrix[i]] = nl[i]
12,867✔
253
      nl[i].bidiDone = true
12,867✔
254
      -- rv[i] = nl[i]
255
   end
256
   n.nodes = SU.compress(rv)
1,124✔
257
end
258

259
function package:bidiEnableTypesetter (typesetter)
60✔
260
   if typesetter.nobidi_boxUpNodes and self.class._initialized then
62✔
261
      return SU.warn("BiDi already enabled, nothing to turn on")
2✔
262
   end
263
   typesetter.nobidi_boxUpNodes = typesetter.boxUpNodes
60✔
264
   typesetter.boxUpNodes = bidiBoxupNodes
60✔
265
end
266

267
function package:bidiDisableTypesetter (typesetter)
60✔
268
   if not typesetter.nobidi_boxUpNodes and self.class._initialized then
4✔
269
      return SU.warn("BiDi not enabled, nothing to turn off")
×
270
   end
271
   typesetter.boxUpNodes = typesetter.nobidi_boxUpNodes
4✔
272
   typesetter.nobidi_boxUpNodes = nil
4✔
273
end
274

275
function package:_init ()
60✔
276
   base._init(self)
61✔
277
   self:deprecatedExport("reorder", self.reorder)
61✔
278
   self:deprecatedExport("bidiEnableTypesetter", self.bidiEnableTypesetter)
61✔
279
   self:deprecatedExport("bidiDisableTypesetter", self.bidiDisableTypesetter)
61✔
280
   if SILE.typesetter then
61✔
281
      self:bidiEnableTypesetter(SILE.typesetter)
1✔
282
   end
283
   self:bidiEnableTypesetter(SILE.typesetters.base)
121✔
284
end
285

286
function package:registerCommands ()
60✔
287
   self:registerCommand("thisframeLTR", function (_, _)
120✔
288
      local direction = "LTR"
×
289
      SILE.typesetter.frame.direction = direction
×
290
      SILE.settings:set("font.direction", direction)
×
291
      SILE.typesetter:leaveHmode()
×
292
      SILE.typesetter.frame:newLine()
×
293
   end)
294

295
   self:registerCommand("thisframedirection", function (options, _)
120✔
296
      local direction = SU.required(options, "direction", "frame direction")
×
297
      SILE.typesetter.frame.direction = direction
×
298
      SILE.settings:set("font.direction", direction)
×
299
      SILE.typesetter:leaveHmode()
×
300
      SILE.typesetter.frame:init()
×
301
   end)
302

303
   self:registerCommand("thisframeRTL", function (_, _)
120✔
304
      local direction = "RTL"
1✔
305
      SILE.typesetter.frame.direction = direction
1✔
306
      SILE.settings:set("font.direction", direction)
1✔
307
      SILE.typesetter:leaveHmode()
1✔
308
      SILE.typesetter.frame:newLine()
1✔
309
   end)
310

311
   self:registerCommand("bidi-on", function (_, _)
120✔
312
      self:bidiEnableTypesetter(SILE.typesetter)
×
313
   end)
314

315
   self:registerCommand("bidi-off", function (_, _)
120✔
UNCOV
316
      self:bidiDisableTypesetter(SILE.typesetter)
×
317
   end)
318
end
319

320
package.documentation = [[
321
\begin{document}
322
Scripts like the Latin alphabet you are currently reading are normally written left to right (LTR); however, some scripts, such as Arabic and Hebrew, are written right to left (RTL).
323
The \autodoc:package{bidi} package, which is loaded by default, provides SILE with the ability to correctly typeset right-to-left text and also documents which mix right-to-left and left-to-right typesetting.
324
Because it is loaded by default, you can use both LTR and RTL text within a paragraph and SILE will ensure that the output characters appear in the correct order.
325

326
The \autodoc:package{bidi} package provides two commands, \autodoc:command{\thisframeLTR} and \autodoc:command{\thisframeRTL}, which set the default text direction for the current frame.
327
If you tell SILE that a frame is RTL, the text will start in the right margin and proceed leftward.
328
It also provides the commands \autodoc:command{\bidi-off} and \autodoc:command{\bidi-on}, which allow you to trade off bidirectional support for a dubious increase in speed.
329
\end{document}
330
]]
60✔
331

332
return package
60✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc