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

sile-typesetter / sile / 11534409649

26 Oct 2024 07:27PM UTC coverage: 33.196% (-28.7%) from 61.897%
11534409649

push

github

alerque
chore(tooling): Update editor-config key for stylua as accepted upstream

Our setting addition is still not in a tagged release, but the PR was
accepted into the default branch of stylua. This means you no longer
need to run my fork of Stylua to get this project's style, you just nead
any build from the main development branch. However the config key was
renamed as part of the acceptance, so this is the relevant adjustment.

5810 of 17502 relevant lines covered (33.2%)

1300.57 hits per line

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

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

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

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

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

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

58
   return matrix
101✔
59
end
60

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

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

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

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

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

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

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

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

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

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

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