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

sile-typesetter / sile / 9441244789

10 Jun 2024 01:45AM UTC coverage: 49.048% (-11.6%) from 60.675%
9441244789

push

github

web-flow
Merge 8a4989eae into 519864108

8320 of 16963 relevant lines covered (49.05%)

1813.12 hits per line

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

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

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

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

8
local function reverse_portion (tbl, s, e)
9
   local rv = {}
×
10
   for i = 1, s - 1 do
×
11
      rv[#rv + 1] = tbl[i]
×
12
   end
13
   for i = e, s, -1 do
×
14
      rv[#rv + 1] = tbl[i]
×
15
   end
16
   for i = e + 1, #tbl do
×
17
      rv[#rv + 1] = tbl[i]
×
18
   end
19
   return rv
×
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
14✔
29
   local matrix = {}
14✔
30
   for i, c in next, line do
220✔
31
      if c.level > max_level then
206✔
32
         max_level = c.level
×
33
      end
34
      matrix[i] = i
206✔
35
   end
36

37
   for level = base_level + 1, max_level do
14✔
38
      local level_start
39
      for i, _ in next, line do
×
40
         if line[i].level >= level then
×
41
            if not level_start then
×
42
               level_start = i
×
43
            elseif i == #line then
×
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
×
50
               local level_end = i - 1
×
51
               matrix = reverse_portion(matrix, level_start, level_end)
×
52
               level_start = nil
×
53
            end
54
         end
55
      end
56
   end
57

58
   return matrix
14✔
59
end
60

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

72
local nodeListToText = function (nl)
73
   local owners, text = {}, {}
14✔
74
   local p = 1
14✔
75
   for i = 1, #nl do
64✔
76
      local n = nl[i]
50✔
77
      if n.text then
50✔
78
         local utfchars = SU.splitUtf8(n.text)
13✔
79
         for j = 1, #utfchars do
247✔
80
            owners[p] = { node = n, pos = j }
234✔
81
            text[p] = utfchars[j]
234✔
82
            p = p + 1
234✔
83
         end
84
      else
85
         owners[p] = { node = n }
37✔
86
         text[p] = luautf8.char(0xFFFC)
37✔
87
         p = p + 1
37✔
88
      end
89
   end
90
   return owners, text
14✔
91
end
92

93
local splitNodeAtPos = function (n, splitstart, p)
94
   if n.is_unshaped then
×
95
      local utf8chars = SU.splitUtf8(n.text)
×
96
      local n2 = SILE.types.node.unshaped({ text = "", options = pl.tablex.copy(n.options) })
×
97
      local n1 = SILE.types.node.unshaped({ text = "", options = pl.tablex.copy(n.options) })
×
98
      for i = splitstart, #utf8chars do
×
99
         if i <= p then
×
100
            n1.text = n1.text .. utf8chars[i]
×
101
         else
102
            n2.text = n2.text .. utf8chars[i]
×
103
         end
104
      end
105
      return n1, n2
×
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
14✔
113
   if #nl == 0 then
14✔
114
      return nl
×
115
   end
116
   local owners, text = nodeListToText(nl)
14✔
117
   local base_level = typesetter.frame:writingDirection() == "RTL" and 1 or 0
28✔
118
   local runs = { icu.bidi_runs(table.concat(text), typesetter.frame:writingDirection()) }
28✔
119
   table.sort(runs, function (a, b)
28✔
120
      return a.start < b.start
×
121
   end)
122
   -- local newNl = {}
123
   -- Split nodes on run boundaries
124
   for i = 1, #runs do
28✔
125
      local run = runs[i]
14✔
126
      local thisOwner = owners[run.start + run.length]
14✔
127
      local nextOwner = owners[run.start + 1 + run.length]
14✔
128
      -- print(thisOwner, nextOwner)
129
      if nextOwner and thisOwner.node == nextOwner.node then
14✔
130
         local before, after = splitNodeAtPos(nextOwner.node, 1, nextOwner.pos - 1)
×
131
         -- print(before, after)
132
         local start = nil
133
         for j = run.start + 1, run.start + run.length do
×
134
            if owners[j].node == nextOwner.node then
×
135
               if not start then
×
136
                  start = j
×
137
               end
138
               owners[j] = { node = before, pos = j - start + 1 }
×
139
            end
140
         end
141
         for j = run.start + 1 + run.length, #owners do
×
142
            if owners[j].node == nextOwner.node then
×
143
               owners[j] = { node = after, pos = j - (run.start + run.length) }
×
144
            end
145
         end
146
      end
147
   end
148
   -- Assign direction/level to nodes
149
   for i = 1, #runs do
28✔
150
      local runstart = runs[i].start + 1
14✔
151
      local runend = runstart + runs[i].length - 1
14✔
152
      for j = runstart, runend do
285✔
153
         if owners[j].node and owners[j].node.options then
271✔
154
            owners[j].node.options.direction = runs[i].dir
234✔
155
            owners[j].node.options.bidilevel = runs[i].level - base_level
234✔
156
         end
157
      end
158
   end
159
   -- String together nodelist
160
   nl = {}
14✔
161
   for i = 1, #owners do
285✔
162
      if #nl and nl[#nl] ~= owners[i].node then
271✔
163
         nl[#nl + 1] = owners[i].node
50✔
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
14✔
169
end
170

171
local bidiBoxupNodes = function (typesetter)
172
   local allDone = true
52✔
173
   for i = 1, #typesetter.state.nodes do
102✔
174
      if not typesetter.state.nodes[i].bidiDone then
63✔
175
         allDone = false
50✔
176
      end
177
   end
178
   if allDone then
52✔
179
      return typesetter:nobidi_boxUpNodes()
38✔
180
   end
181
   local newNodeList = splitNodelistIntoBidiRuns(typesetter)
14✔
182
   typesetter:shapeAllNodes(newNodeList)
14✔
183
   typesetter.state.nodes = newNodeList
14✔
184
   local vboxlist = typesetter:nobidi_boxUpNodes()
14✔
185
   -- Scan for out-of-direction material
186
   for i = 1, #vboxlist do
42✔
187
      local v = vboxlist[i]
28✔
188
      if v.is_vbox then
28✔
189
         package.reorder(nil, v, typesetter)
14✔
190
      end
191
   end
192
   return vboxlist
14✔
193
end
194

195
function package.reorder (_, n, typesetter)
3✔
196
   local nl = n.nodes
14✔
197
   -- local newNl = {}
198
   -- local matrix = {}
199
   local levels = {}
14✔
200
   local base_level = typesetter.frame:writingDirection() == "RTL" and 1 or 0
28✔
201
   for i = 1, #nl do
220✔
202
      if nl[i].options and nl[i].options.bidilevel then
206✔
203
         levels[i] = { level = nl[i].options.bidilevel }
57✔
204
      end
205
   end
206
   for i = 1, #nl do
220✔
207
      if not levels[i] then
206✔
208
         -- resolve neutrals
209
         local left_level, right_level
210
         for left = i - 1, 1, -1 do
449✔
211
            if nl[left].options and nl[left].options.bidilevel then
375✔
212
               left_level = nl[left].options.bidilevel
75✔
213
               break
75✔
214
            end
215
         end
216
         for right = i + 1, #nl do
449✔
217
            if nl[right].options and nl[right].options.bidilevel then
368✔
218
               right_level = nl[right].options.bidilevel
68✔
219
               break
68✔
220
            end
221
         end
222
         levels[i] = { level = (left_level == right_level and left_level or 0) }
149✔
223
      end
224
   end
225
   local matrix = create_matrix(levels, 0)
14✔
226
   local rv = {}
14✔
227
   -- for i = 1, #nl do print(i, nl[i], levels[i]) end
228
   for i = 1, #nl do
220✔
229
      if nl[i].is_nnode and levels[i].level % 2 ~= base_level then
206✔
230
         SU.flip_in_place(nl[i].nodes)
×
231
         reverse_each_node(nl[i].nodes)
×
232
      elseif nl[i].is_discretionary and levels[i].level % 2 ~= base_level and not nl[i].bidiDone then
206✔
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]
206✔
253
      nl[i].bidiDone = true
206✔
254
      -- rv[i] = nl[i]
255
   end
256
   n.nodes = SU.compress(rv)
28✔
257
end
258

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

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

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

286
function package:registerCommands ()
3✔
287
   self:registerCommand("thisframeLTR", function (_, _)
6✔
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, _)
6✔
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 (_, _)
6✔
304
      local direction = "RTL"
×
305
      SILE.typesetter.frame.direction = direction
×
306
      SILE.settings:set("font.direction", direction)
×
307
      SILE.typesetter:leaveHmode()
×
308
      SILE.typesetter.frame:newLine()
×
309
   end)
310

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

315
   self:registerCommand("bidi-off", function (_, _)
6✔
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
]]
3✔
331

332
return package
3✔
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