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

sile-typesetter / sile / 6934957716

20 Nov 2023 07:35PM UTC coverage: 57.468% (-3.2%) from 60.703%
6934957716

push

github

web-flow
Merge c91d9a7d4 into 34e2e5335

60 of 79 new or added lines in 1 file covered. (75.95%)

717 existing lines in 27 files now uncovered.

8957 of 15586 relevant lines covered (57.47%)

5715.38 hits per line

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

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

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

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

8
local function reverse_portion (tbl, s, e)
UNCOV
9
  local rv = {}
×
UNCOV
10
  for i = 1, s-1 do rv[#rv+1] = tbl[i] end
×
UNCOV
11
  for i = e, s, -1 do rv[#rv+1] = tbl[i] end
×
UNCOV
12
  for i = e+1, #tbl do rv[#rv+1] = tbl[i] end
×
UNCOV
13
  return rv
×
14
end
15

16
local function create_matrix (line, base_level)
17
  -- L2; create a transformation matrix of elements
18
  -- such that output[matrix[i]] = input[i]
19
  -- e.g. No reversions required: [1, 2, 3, 4, 5]
20
  -- Levels [0, 0, 0, 1, 1] -> [1, 2, 3, 5, 4]
21

22
  local max_level = 0
155✔
23
  local matrix = {}
155✔
24
  for i, c in next, line do
3,162✔
25
    if c.level > max_level then max_level = c.level end
3,007✔
26
    matrix[i] = i
3,007✔
27
  end
28

29
  for level = base_level+1, max_level do
155✔
30
    local level_start
UNCOV
31
    for i, _ in next, line do
×
UNCOV
32
      if line[i].level >= level then
×
UNCOV
33
        if not level_start then
×
UNCOV
34
          level_start = i
×
UNCOV
35
        elseif i == #line then
×
36
          local level_end = i
×
37
          matrix = reverse_portion(matrix, level_start, level_end)
×
38
          level_start = nil
×
39
        end
40
      else
UNCOV
41
        if level_start then
×
UNCOV
42
          local level_end = i-1
×
UNCOV
43
          matrix = reverse_portion(matrix, level_start, level_end)
×
UNCOV
44
          level_start = nil
×
45
        end
46
      end
47
    end
48
  end
49

50
  return matrix
155✔
51
end
52

53
local function reverse_each_node (nodelist)
UNCOV
54
  for j = 1, #nodelist do
×
UNCOV
55
    if nodelist[j].type =="hbox" then
×
UNCOV
56
      if nodelist[j].value.items then SU.flip_in_place(nodelist[j].value.items) end
×
UNCOV
57
      SU.flip_in_place(nodelist[j].value.glyphString)
×
58
    end
59
  end
60
end
61

62
local nodeListToText = function (nl)
63
  local owners, text = {}, {}
112✔
64
  local p = 1
112✔
65
  for i = 1, #nl do local n = nl[i]
758✔
66
    if n.text then
379✔
67
      local utfchars = SU.splitUtf8(n.text)
101✔
68
      for j = 1, #utfchars do
4,337✔
69
        owners[p] = { node = n, pos = j }
4,236✔
70
        text[p] = utfchars[j]
4,236✔
71
        p = p + 1
4,236✔
72
      end
73
    else
74
      owners[p] = { node = n }
278✔
75
      text[p] = luautf8.char(0xFFFC)
278✔
76
      p = p + 1
278✔
77
    end
78
  end
79
  return owners, text
112✔
80
end
81

82
local splitNodeAtPos = function (n, splitstart, p)
UNCOV
83
  if n.is_unshaped then
×
UNCOV
84
    local utf8chars = SU.splitUtf8(n.text)
×
UNCOV
85
    local n2 = SILE.nodefactory.unshaped({ text = "", options = pl.tablex.copy(n.options) })
×
UNCOV
86
    local n1 = SILE.nodefactory.unshaped({ text = "", options = pl.tablex.copy(n.options) })
×
UNCOV
87
    for i = splitstart, #utf8chars do
×
UNCOV
88
      if i <= p then n1.text = n1.text .. utf8chars[i]
×
UNCOV
89
      else n2.text = n2.text .. utf8chars[i]
×
90
      end
91
    end
UNCOV
92
    return n1, n2
×
93
  else
94
    SU.error("Unsure how to split node " .. tostring(n) .. " at position " .. p, true)
×
95
  end
96
end
97

98
local splitNodelistIntoBidiRuns = function (typesetter)
99
  local nl = typesetter.state.nodes
112✔
100
  if #nl == 0 then return nl end
112✔
101
  local owners, text = nodeListToText(nl)
112✔
102
  local base_level = typesetter.frame:writingDirection() == "RTL" and 1 or 0
224✔
103
  local runs = { icu.bidi_runs(table.concat(text), typesetter.frame:writingDirection()) }
224✔
104
  table.sort(runs, function (a, b) return a.start < b.start end)
112✔
105
  -- local newNl = {}
106
  -- Split nodes on run boundaries
107
  for i = 1, #runs do
224✔
108
    local run = runs[i]
112✔
109
    local thisOwner = owners[run.start+run.length]
112✔
110
    local nextOwner = owners[run.start+1+run.length]
112✔
111
    -- print(thisOwner, nextOwner)
112
    if nextOwner and thisOwner.node == nextOwner.node then
112✔
UNCOV
113
      local before, after = splitNodeAtPos(nextOwner.node, 1, nextOwner.pos-1)
×
114
      -- print(before, after)
115
      local start = nil
UNCOV
116
      for j = run.start+1, run.start+run.length do
×
UNCOV
117
        if owners[j].node==nextOwner.node then
×
UNCOV
118
          if not start then start = j end
×
UNCOV
119
          owners[j]={ node=before ,pos=j-start+1 }
×
120
        end
121
      end
UNCOV
122
      for j = run.start + 1 + run.length, #owners do
×
UNCOV
123
        if owners[j].node==nextOwner.node then
×
UNCOV
124
          owners[j] = { node = after, pos = j - (run.start + run.length) }
×
125
        end
126
      end
127
    end
128
  end
129
  -- Assign direction/level to nodes
130
  for i = 1, #runs do
224✔
131
    local runstart = runs[i].start+1
112✔
132
    local runend   = runstart + runs[i].length-1
112✔
133
    for j= runstart, runend do
4,626✔
134
      if owners[j].node and owners[j].node.options then
4,514✔
135
        owners[j].node.options.direction = runs[i].dir
4,236✔
136
        owners[j].node.options.bidilevel = runs[i].level - base_level
4,236✔
137
      end
138
    end
139
  end
140
  -- String together nodelist
141
  nl={}
112✔
142
  for i = 1, #owners do
4,626✔
143
    if #nl and nl[#nl] ~= owners[i].node then
4,514✔
144
      nl[#nl+1] = owners[i].node
379✔
145
      -- print(nl[#nl], nl[#nl].options)
146
    end
147
  end
148
  -- for i = 1, #nl do print(i, nl[i]) end
149
  return nl
112✔
150
end
151

152
local bidiBoxupNodes = function (typesetter)
153
  local allDone = true
275✔
154
  for i = 1, #typesetter.state.nodes do
654✔
155
    if not typesetter.state.nodes[i].bidiDone then allDone = false end
480✔
156
  end
157
  if allDone then return typesetter:nobidi_boxUpNodes() end
275✔
158
  local newNodeList = splitNodelistIntoBidiRuns(typesetter)
112✔
159
  typesetter:shapeAllNodes(newNodeList)
112✔
160
  typesetter.state.nodes = newNodeList
112✔
161
  local vboxlist = typesetter:nobidi_boxUpNodes()
112✔
162
  -- Scan for out-of-direction material
163
  for i = 1, #vboxlist do
440✔
164
    local v = vboxlist[i]
328✔
165
    if v.is_vbox then package.reorder(nil, v, typesetter) end
328✔
166
  end
167
  return vboxlist
112✔
168
end
169

170
function package.reorder (_, n, typesetter)
13✔
171
  local nl = n.nodes
155✔
172
  -- local newNl = {}
173
  -- local matrix = {}
174
  local levels = {}
155✔
175
  local base_level = typesetter.frame:writingDirection() == "RTL" and 1 or 0
310✔
176
  for i = 1, #nl do
3,162✔
177
    if nl[i].options and nl[i].options.bidilevel then
3,007✔
178
      levels[i] = { level = nl[i].options.bidilevel }
1,050✔
179
    end
180
  end
181
  for i = 1, #nl do
3,162✔
182
    if not levels[i] then
3,007✔
183
      -- resolve neutrals
184
      local left_level, right_level
185
      for left = i - 1, 1, -1 do
4,380✔
186
        if nl[left].options and nl[left].options.bidilevel then
3,687✔
187
          left_level = nl[left].options.bidilevel
1,264✔
188
          break
1,264✔
189
        end
190
      end
191
      for right = i + 1, #nl do
4,380✔
192
        if nl[right].options and nl[right].options.bidilevel then
3,618✔
193
          right_level = nl[right].options.bidilevel
1,195✔
194
          break
1,195✔
195
        end
196
      end
197
      levels[i] = { level = (left_level == right_level and left_level or 0) }
1,957✔
198
    end
199
  end
200
  local matrix = create_matrix(levels, 0)
155✔
201
  local rv = {}
155✔
202
  -- for i = 1, #nl do print(i, nl[i], levels[i]) end
203
  for i = 1, #nl do
3,162✔
204
    if nl[i].is_nnode and levels[i].level %2 ~= base_level then
3,007✔
UNCOV
205
      SU.flip_in_place(nl[i].nodes)
×
UNCOV
206
      reverse_each_node(nl[i].nodes)
×
207
    elseif nl[i].is_discretionary and levels[i].level %2 ~= base_level and not nl[i].bidiDone then
3,007✔
208
      for j = 1, #(nl[i].replacement) do
×
209
        if nl[i].replacement[j].is_nnode then
×
210
          SU.flip_in_place(nl[i].replacement[j].nodes)
×
211
          reverse_each_node(nl[i].replacement[j].nodes)
×
212
        end
213
      end
214
      for j = 1, #(nl[i].prebreak) do
×
215
        if nl[i].prebreak[j].is_nnode then
×
216
          SU.flip_in_place(nl[i].prebreak[j].nodes)
×
217
          reverse_each_node(nl[i].prebreak[j].nodes)
×
218
        end
219
      end
220
      for j = 1, #(nl[i].postbreak) do
×
221
        if nl[i].postbreak[j].is_nnode then
×
222
          SU.flip_in_place(nl[i].postbreak[j].nodes)
×
223
          reverse_each_node(nl[i].postbreak[j].nodes)
×
224
        end
225
      end
226

227
    end
228
    rv[matrix[i]] = nl[i]
3,007✔
229
    nl[i].bidiDone = true
3,007✔
230
    -- rv[i] = nl[i]
231
  end
232
  n.nodes = SU.compress(rv)
310✔
233
end
234

235
function package:bidiEnableTypesetter (typesetter)
13✔
236
  if typesetter.nobidi_boxUpNodes and self.class._initialized then
13✔
237
    return SU.warn("BiDi already enabled, nothing to turn on")
×
238
  end
239
  typesetter.nobidi_boxUpNodes = typesetter.boxUpNodes
13✔
240
  typesetter.boxUpNodes = bidiBoxupNodes
13✔
241
end
242

243
function package:bidiDisableTypesetter (typesetter)
13✔
UNCOV
244
  if not typesetter.nobidi_boxUpNodes and self.class._initialized then
×
245
    return SU.warn("BiDi not enabled, nothing to turn off")
×
246
  end
UNCOV
247
  typesetter.boxUpNodes = typesetter.nobidi_boxUpNodes
×
UNCOV
248
  typesetter.nobidi_boxUpNodes = nil
×
249
end
250

251
function package:_init ()
13✔
252
  base._init(self)
13✔
253
  self:deprecatedExport("reorder", self.reorder)
13✔
254
  self:deprecatedExport("bidiEnableTypesetter", self.bidiEnableTypesetter)
13✔
255
  self:deprecatedExport("bidiDisableTypesetter", self.bidiDisableTypesetter)
13✔
256
  if SILE.typesetter then
13✔
257
    self:bidiEnableTypesetter(SILE.typesetter)
×
258
  end
259
  self:bidiEnableTypesetter(SILE.typesetters.base)
26✔
260
end
261

262
function package:registerCommands ()
13✔
263

264
  self:registerCommand("thisframeLTR", function (_, _)
26✔
265
    local direction = "LTR"
×
266
    SILE.typesetter.frame.direction = direction
×
267
    SILE.settings:set("font.direction", direction)
×
268
    SILE.typesetter:leaveHmode()
×
269
    SILE.typesetter.frame:newLine()
×
270
  end)
271

272
  self:registerCommand("thisframedirection", function (options, _)
26✔
273
    local direction = SU.required(options, "direction", "frame direction")
×
274
    SILE.typesetter.frame.direction = direction
×
275
    SILE.settings:set("font.direction", direction)
×
276
    SILE.typesetter:leaveHmode()
×
277
    SILE.typesetter.frame:init()
×
278
  end)
279

280
  self:registerCommand("thisframeRTL", function (_, _)
26✔
281
    local direction = "RTL"
×
282
    SILE.typesetter.frame.direction = direction
×
283
    SILE.settings:set("font.direction", direction)
×
284
    SILE.typesetter:leaveHmode()
×
285
    SILE.typesetter.frame:newLine()
×
286
  end)
287

288
  self:registerCommand("bidi-on", function (_, _)
26✔
289
    self:bidiEnableTypesetter(SILE.typesetter)
×
290
  end)
291

292
  self:registerCommand("bidi-off", function (_, _)
26✔
293
    self:bidiDisableTypesetter(SILE.typesetter)
×
294
  end)
295

296
end
297

298
package.documentation = [[
299
\begin{document}
300
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).
301
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.
302
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.
303

304
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.
305
If you tell SILE that a frame is RTL, the text will start in the right margin and proceed leftward.
306
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.
307
\end{document}
308
]]
13✔
309

310
return package
13✔
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