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

sile-typesetter / sile / 14958605596

11 May 2025 06:34PM UTC coverage: 31.311% (-25.4%) from 56.689%
14958605596

push

github

web-flow
Merge 3e53926d5 into 443551a3e

6301 of 20124 relevant lines covered (31.31%)

4203.99 hits per line

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

0.0
/languages/fr/init.lua
1
--- French language rules
2
-- @submodule languages
3

4
local computeSpaces = function ()
5
   -- Computes:
6
   --  -  regular inter-word space,
7
   --  -  half inter-word fixed space,
8
   --  -  "guillemet space", as defined in LaTeX's babel-french which is based
9
   --     on Thierry Bouche's recommendations,
10
   --  These should be usual for France and Canada. The Swiss may prefer a thin
11
   --  space for guillemets, that's why we are having settings hereafter.
12
   local enlargement = SILE.settings:get("shaper.spaceenlargementfactor")
×
13
   local stretch = SILE.settings:get("shaper.spacestretchfactor")
×
14
   local shrink = SILE.settings:get("shaper.spaceshrinkfactor")
×
15
   return {
×
16
      colonspace = SILE.types.length(enlargement .. "spc plus " .. stretch .. "spc minus " .. shrink .. "spc"),
17
      thinspace = SILE.types.length((0.5 * enlargement) .. "spc"),
18
      guillspace = SILE.types.length(
×
19
         (0.8 * enlargement) .. "spc plus " .. (0.3 * stretch) .. "spc minus " .. (0.8 * shrink) .. "spc"
×
20
      ),
×
21
   }
22
end
23

24
local spaces = computeSpaces()
×
25
-- NOTE: We are only doing it at load time. We don't expect the shaper settings to be often
26
-- changed arbitrarily _after_ having selected a language...
27

28
SILE.settings:declare({
×
29
   parameter = "languages.fr.colonspace",
30
   type = "kern",
31
   default = SILE.types.node.kern(spaces.colonspace),
32
   help = "The amount of space before a colon, theoretically a non-breakable, shrinkable, stretchable inter-word space",
33
})
34

35
SILE.settings:declare({
×
36
   parameter = "languages.fr.thinspace",
37
   type = "kern",
38
   default = SILE.types.node.kern(spaces.thinspace),
39
   help = "The amount of space before high punctuations, theoretically a fixed, non-breakable space, around half the inter-word space",
40
})
41

42
SILE.settings:declare({
×
43
   parameter = "languages.fr.guillspace",
44
   type = "kern",
45
   default = SILE.types.node.kern(spaces.guillspace),
46
   help = "The amount of space applying to guillemets, theoretically smaller than a non-breakable inter-word space, with reduced stretchability",
47
})
48

49
SILE.settings:declare({
×
50
   parameter = "languages.fr.debugspace",
51
   type = "boolean",
52
   default = false,
53
   help = "If switched to true, uses large spaces instead of the regular punctuation ones",
54
})
55

56
local getSpaceGlue = function (options, parameter)
57
   local sg
58
   if SILE.settings:get("languages.fr.debugspace") then
×
59
      sg = SILE.types.node.kern("5spc")
×
60
   else
61
      sg = SILE.settings:get(parameter)
×
62
   end
63
   -- Return the absolute (kern) length of the specified spacing parameter
64
   -- with a particular set of font options.
65
   -- As for SILE.shapers.base.measureSpace(), which has the same type of
66
   -- logic, caching this doesn't seem to have any significant speedup.
67
   SILE.settings:temporarily(function ()
×
68
      SILE.settings:set("font.size", options.size)
×
69
      SILE.settings:set("font.family", options.family)
×
70
      SILE.settings:set("font.filename", options.filename)
×
71
      sg = sg:absolute()
×
72
   end)
73
   -- Track a subtype on that kern:
74
   -- See automated italic correction at the typesetter level.
75
   sg.subtype = "punctspace"
×
76
   return sg
×
77
end
78

79
SILE.nodeMakers.fr = pl.class(SILE.nodeMakers.unicode)
×
80

81
-- Unfortunately, there is nothing in the Unicode properties
82
-- database which distinguishes between high and low punctuation, etc.
83
-- But in a way that's precisely why we can't just rely on Unicode
84
-- for everything and need our language-specific typesetting
85
-- processors.
86
SILE.nodeMakers.fr.colonPunctuations = { ":" }
×
87
SILE.nodeMakers.fr.openingQuotes = { "«", "‹" }
×
88
SILE.nodeMakers.fr.closingQuotes = { "»", "›" }
×
89
-- There's catch below: the shaper may have already processed common ligatures (!!, ?!, !?)
90
-- as a single item...
91
SILE.nodeMakers.fr.highPunctuations = { ";", "!", "?", "!!", "?!", "!?" }
×
92
-- High punctuations have some (kern) space before them... except in some cases!
93
-- By the books, they have it "after a letter or digit", at least. After a closing
94
-- punctuation, too, seems usual.
95
-- Otherwise, one shall have no space inside e.g. (?), ?!, [!], …?, !!! etc.
96
-- As a simplification, we reverse the rule and define after which characters the space
97
-- shall not be added. This is by no mean perfect, I couldn't find an explicit list
98
-- of exceptions. French typography is a delicate beast.
99
SILE.nodeMakers.fr.spaceExceptions =
×
100
   { "!", "?", ":", ".", "…", "(", "[", "{", "<", "«", "‹", "“", "‘", "?!", "!!", "!?" }
×
101

102
-- overridden properties from parent class
103
SILE.nodeMakers.fr.quoteTypes = { qu = true } -- split tokens at apostrophes &c.
×
104

105
-- methods defined in this class
106

107
function SILE.nodeMakers.fr:isIn (set, text)
×
108
   for _, v in ipairs(set) do
×
109
      if v == text then
×
110
         return true
×
111
      end
112
   end
113
   return false
×
114
end
115

116
function SILE.nodeMakers.fr:isOpeningQuote (text)
×
117
   return self:isIn(self.openingQuotes, text)
×
118
end
119

120
function SILE.nodeMakers.fr:isClosingQuote (text)
×
121
   return self:isIn(self.closingQuotes, text)
×
122
end
123

124
function SILE.nodeMakers.fr:isColonPunctuation (text)
×
125
   return self:isIn(self.colonPunctuations, text)
×
126
end
127

128
function SILE.nodeMakers.fr:isHighPunctuation (text)
×
129
   return self:isIn(self.highPunctuations, text)
×
130
end
131

132
function SILE.nodeMakers.fr:isSpaceException (text)
×
133
   return self:isIn(self.spaceExceptions, text)
×
134
end
135

136
function SILE.nodeMakers.fr:isPrevSpaceException ()
×
137
   return self.i > 1 and self:isSpaceException(self.items[self.i - 1].text) or false
×
138
end
139

140
function SILE.nodeMakers.fr:makeUnbreakableSpace (parameter)
×
141
   self:makeToken()
×
142
   self.lastnode = "glue"
×
143
   coroutine.yield(getSpaceGlue(self.options, parameter))
×
144
end
145

146
function SILE.nodeMakers.fr:handleSpaceBefore (item)
×
147
   if self:isHighPunctuation(item.text) and not self:isPrevSpaceException() then
×
148
      self:makeUnbreakableSpace("languages.fr.thinspace")
×
149
      self:makeToken()
×
150
      self:addToken(item.text, item)
×
151
      return true
×
152
   end
153
   if self:isColonPunctuation(item.text) and not self:isPrevSpaceException() then
×
154
      self:makeUnbreakableSpace("languages.fr.colonspace")
×
155
      self:makeToken()
×
156
      self:addToken(item.text, item)
×
157
      return true
×
158
   end
159
   if self:isClosingQuote(item.text) then
×
160
      self:makeUnbreakableSpace("languages.fr.guillspace")
×
161
      self:makeToken()
×
162
      self:addToken(item.text, item)
×
163
      return true
×
164
   end
165
   return false
×
166
end
167

168
function SILE.nodeMakers.fr:handleSpaceAfter (item)
×
169
   if self:isOpeningQuote(item.text) then
×
170
      self:addToken(item.text, item)
×
171
      self:makeUnbreakableSpace("languages.fr.guillspace")
×
172
      self:makeToken()
×
173
      return true
×
174
   end
175
   return false
×
176
end
177

178
function SILE.nodeMakers.fr:mustRemove (i, items)
×
179
   -- Clear "manual" spaces we do not want, so that later we only have to
180
   -- insert the relevant kerns.
181
   local curr = items[i].text
×
182
   if self:isSpace(curr) or self:isNonBreakingSpace(curr) then
×
183
      if i < #items then
×
184
         local next = items[i + 1].text
×
185
         if
186
            self:isSpace(next)
×
187
            or self:isNonBreakingSpace(next)
×
188
            or self:isHighPunctuation(next)
×
189
            or self:isColonPunctuation(next)
×
190
            or self:isClosingQuote(next)
×
191
         then
192
            return true
×
193
         end
194
      end
195
      if i > 1 then
×
196
         local prev = items[i - 1].text
×
197
         if self:isOpeningQuote(prev) then
×
198
            return true
×
199
         end
200
      end
201
   end
202
   return false
×
203
end
204

205
-- overridden methods from parent class
206

207
function SILE.nodeMakers.fr:dealWith (item)
×
208
   if self:handleSpaceBefore(item) then
×
209
      return
×
210
   end
211
   if self:handleSpaceAfter(item) then
×
212
      return
×
213
   end
214
   self._base.dealWith(self, item)
×
215
end
216

217
function SILE.nodeMakers.fr:handleWordBreak (item)
×
218
   if self:handleSpaceBefore(item) then
×
219
      return
×
220
   end
221
   if self:handleSpaceAfter(item) then
×
222
      return
×
223
   end
224
   self._base.handleWordBreak(self, item)
×
225
end
226

227
function SILE.nodeMakers.fr:handleLineBreak (item, subtype)
×
228
   if self:isSpace(item.text) then
×
229
      self:handleWordBreak(item)
×
230
      return
×
231
   end
232
   if self:handleSpaceBefore(item) then
×
233
      return
×
234
   end
235
   if self:handleSpaceAfter(item) then
×
236
      return
×
237
   end
238

239
   self._base.handleLineBreak(self, item, subtype)
×
240
end
241

242
function SILE.nodeMakers.fr:iterator (items)
×
243
   -- We start by cleaning up the input once for all.
244
   local cleanItems = {}
×
245
   local removed = 0
×
246
   for k = 1, #items do
×
247
      if self:mustRemove(k, items) then
×
248
         -- the index is actually a character position in the byte stream.
249
         -- So we need to take its actual byte length into account.
250
         -- For instance, U+00A0 NBSP is 2 bytes long (0xC2 0xA0) in UTF-8.
251
         removed = removed + string.len(items[k].text)
×
252
      else
253
         -- index has changed due to removals
254
         items[k].index = items[k].index - removed
×
255
         table.insert(cleanItems, items[k])
×
256
      end
257
   end
258
   return self._base.iterator(self, cleanItems)
×
259
end
260

261
local hyphens = require("languages.fr.hyphens-tex")
×
262
SILE.hyphenator.languages["fr"] = hyphens
×
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