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

sile-typesetter / sile / 9409557472

07 Jun 2024 12:09AM UTC coverage: 69.448% (-4.5%) from 73.988%
9409557472

push

github

alerque
fix(build): Distribute vendored compat-5.3.c source file

12025 of 17315 relevant lines covered (69.45%)

6023.46 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")
80✔
2

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

6
local icu = require("justenoughicu")
80✔
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
791✔
29
   local matrix = {}
791✔
30
   for i, c in next, line do
17,586✔
31
      if c.level > max_level then
16,795✔
32
         max_level = c.level
11✔
33
      end
34
      matrix[i] = i
16,795✔
35
   end
36

37
   for level = base_level + 1, max_level do
802✔
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
791✔
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 = {}, {}
488✔
74
   local p = 1
488✔
75
   for i = 1, #nl do
2,768✔
76
      local n = nl[i]
2,280✔
77
      if n.text then
2,280✔
78
         local utfchars = SU.splitUtf8(n.text)
803✔
79
         for j = 1, #utfchars do
24,233✔
80
            owners[p] = { node = n, pos = j }
23,430✔
81
            text[p] = utfchars[j]
23,430✔
82
            p = p + 1
23,430✔
83
         end
84
      else
85
         owners[p] = { node = n }
1,477✔
86
         text[p] = luautf8.char(0xFFFC)
1,477✔
87
         p = p + 1
1,477✔
88
      end
89
   end
90
   return owners, text
488✔
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
488✔
113
   if #nl == 0 then
488✔
114
      return nl
×
115
   end
116
   local owners, text = nodeListToText(nl)
488✔
117
   local base_level = typesetter.frame:writingDirection() == "RTL" and 1 or 0
976✔
118
   local runs = { icu.bidi_runs(table.concat(text), typesetter.frame:writingDirection()) }
976✔
119
   table.sort(runs, function (a, b)
976✔
120
      return a.start < b.start
69✔
121
   end)
122
   -- local newNl = {}
123
   -- Split nodes on run boundaries
124
   for i = 1, #runs do
1,002✔
125
      local run = runs[i]
514✔
126
      local thisOwner = owners[run.start + run.length]
514✔
127
      local nextOwner = owners[run.start + 1 + run.length]
514✔
128
      -- print(thisOwner, nextOwner)
129
      if nextOwner and thisOwner.node == nextOwner.node then
514✔
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
1,002✔
150
      local runstart = runs[i].start + 1
514✔
151
      local runend = runstart + runs[i].length - 1
514✔
152
      for j = runstart, runend do
25,421✔
153
         if owners[j].node and owners[j].node.options then
24,907✔
154
            owners[j].node.options.direction = runs[i].dir
23,430✔
155
            owners[j].node.options.bidilevel = runs[i].level - base_level
23,430✔
156
         end
157
      end
158
   end
159
   -- String together nodelist
160
   nl = {}
488✔
161
   for i = 1, #owners do
25,395✔
162
      if #nl and nl[#nl] ~= owners[i].node then
24,907✔
163
         nl[#nl + 1] = owners[i].node
2,303✔
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
488✔
169
end
170

171
local bidiBoxupNodes = function (typesetter)
172
   local allDone = true
1,091✔
173
   for i = 1, #typesetter.state.nodes do
3,371✔
174
      if not typesetter.state.nodes[i].bidiDone then
2,981✔
175
         allDone = false
2,072✔
176
      end
177
   end
178
   if allDone then
1,091✔
179
      return typesetter:nobidi_boxUpNodes()
603✔
180
   end
181
   local newNodeList = splitNodelistIntoBidiRuns(typesetter)
488✔
182
   typesetter:shapeAllNodes(newNodeList)
488✔
183
   typesetter.state.nodes = newNodeList
488✔
184
   local vboxlist = typesetter:nobidi_boxUpNodes()
488✔
185
   -- Scan for out-of-direction material
186
   for i = 1, #vboxlist do
2,247✔
187
      local v = vboxlist[i]
1,759✔
188
      if v.is_vbox then
1,759✔
189
         package.reorder(nil, v, typesetter)
791✔
190
      end
191
   end
192
   return vboxlist
488✔
193
end
194

195
function package.reorder (_, n, typesetter)
80✔
196
   local nl = n.nodes
791✔
197
   -- local newNl = {}
198
   -- local matrix = {}
199
   local levels = {}
791✔
200
   local base_level = typesetter.frame:writingDirection() == "RTL" and 1 or 0
1,582✔
201
   for i = 1, #nl do
17,586✔
202
      if nl[i].options and nl[i].options.bidilevel then
16,795✔
203
         levels[i] = { level = nl[i].options.bidilevel }
6,056✔
204
      end
205
   end
206
   for i = 1, #nl do
17,586✔
207
      if not levels[i] then
16,795✔
208
         -- resolve neutrals
209
         local left_level, right_level
210
         for left = i - 1, 1, -1 do
21,029✔
211
            if nl[left].options and nl[left].options.bidilevel then
17,844✔
212
               left_level = nl[left].options.bidilevel
7,554✔
213
               break
7,554✔
214
            end
215
         end
216
         for right = i + 1, #nl do
21,029✔
217
            if nl[right].options and nl[right].options.bidilevel then
17,674✔
218
               right_level = nl[right].options.bidilevel
7,384✔
219
               break
7,384✔
220
            end
221
         end
222
         levels[i] = { level = (left_level == right_level and left_level or 0) }
10,739✔
223
      end
224
   end
225
   local matrix = create_matrix(levels, 0)
791✔
226
   local rv = {}
791✔
227
   -- for i = 1, #nl do print(i, nl[i], levels[i]) end
228
   for i = 1, #nl do
17,586✔
229
      if nl[i].is_nnode and levels[i].level % 2 ~= base_level then
16,795✔
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
16,699✔
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]
16,795✔
253
      nl[i].bidiDone = true
16,795✔
254
      -- rv[i] = nl[i]
255
   end
256
   n.nodes = SU.compress(rv)
1,582✔
257
end
258

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

267
function package:bidiDisableTypesetter (typesetter)
80✔
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 ()
80✔
276
   base._init(self)
81✔
277
   self:deprecatedExport("reorder", self.reorder)
81✔
278
   self:deprecatedExport("bidiEnableTypesetter", self.bidiEnableTypesetter)
81✔
279
   self:deprecatedExport("bidiDisableTypesetter", self.bidiDisableTypesetter)
81✔
280
   if SILE.typesetter then
81✔
281
      self:bidiEnableTypesetter(SILE.typesetter)
1✔
282
   end
283
   self:bidiEnableTypesetter(SILE.typesetters.base)
161✔
284
end
285

286
function package:registerCommands ()
80✔
287
   self:registerCommand("thisframeLTR", function (_, _)
160✔
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, _)
160✔
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 (_, _)
160✔
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 (_, _)
160✔
312
      self:bidiEnableTypesetter(SILE.typesetter)
×
313
   end)
314

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

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

© 2025 Coveralls, Inc