• 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

0.0
/packages/bibtex/bibliography.lua
1
-- luacheck: globals setfenv getfenv
2
-- luacheck: ignore _ENV
3

4
-- The following functions borrowed from Norman Ramsey's nbibtex,
5
-- with permission.
6

7
local function find_outside_braces (str, pat, i)
8
   -- local len = string.len(str)
9
   local j, k = string.find(str, pat, i)
×
10
   if not j then
×
11
      return j, k
×
12
   end
13
   local jb, kb = string.find(str, "%b{}", i)
×
14
   while jb and jb < j do -- scan past braces
×
15
      -- braces come first, so we search again after close brace
16
      local i2 = kb + 1
×
17
      j, k = string.find(str, pat, i2)
×
18
      if not j then
×
19
         return j, k
×
20
      end
21
      jb, kb = string.find(str, "%b{}", i2)
×
22
   end
23
   -- either pat precedes braces or there are no braces
24
   return string.find(str, pat, j) -- 2nd call needed to get captures
×
25
end
26

27
local function split (str, pat, find) -- return list of substrings separated by pat
28
   find = find or string.find -- could be find_outside_braces
×
29
   -- @Omikhelia: I added this check here to avoid breaking on error,
30
   -- but probably in could have been done earlier...
31
   if not str then
×
32
      return {}
×
33
   end
34

35
   local len = string.len(str)
×
36
   local t = {}
×
37
   local insert = table.insert
×
38
   local i, j = 1, true
×
39
   local k
40
   while j and i <= len + 1 do
×
41
      j, k = find(str, pat, i)
×
42
      if j then
×
43
         insert(t, string.sub(str, i, j - 1))
×
44
         i = k + 1
×
45
      else
46
         insert(t, string.sub(str, i))
×
47
      end
48
   end
49
   return t
×
50
end
51

52
local function splitters (str, pat, find) -- return list of separators
53
   find = find or string.find -- could be find_outside_braces
×
54
   local t = {}
×
55
   local insert = table.insert
×
56
   local j, k = find(str, pat, 1)
×
57
   while j do
×
58
      insert(t, string.sub(str, j, k))
×
59
      j, k = find(str, pat, k + 1)
×
60
   end
61
   return t
×
62
end
63

64
local function namesplit (str)
65
   local t = split(str, "%s+[aA][nN][dD]%s+", find_outside_braces)
×
66
   local i = 2
×
67
   while i <= #t do
×
68
      while string.find(t[i], "^[aA][nN][dD]%s+") do
×
69
         t[i] = string.gsub(t[i], "^[aA][nN][dD]%s+", "")
×
70
         table.insert(t, i, "")
×
71
         i = i + 1
×
72
      end
73
      i = i + 1
×
74
   end
75
   return t
×
76
end
77

78
local sep_and_not_tie = "%-"
×
79
local sep_chars = sep_and_not_tie .. "%~"
×
80

81
local parse_name
82
do
83
   local white_sep = "[" .. sep_chars .. "%s]+"
×
84
   local white_comma_sep = "[" .. sep_chars .. "%s%,]+"
×
85
   local trailing_commas = "(,[" .. sep_chars .. "%s%,]*)$"
×
86
   local sep_char = "[" .. sep_chars .. "]"
×
87
   local leading_white_sep = "^" .. white_sep
×
88

89
   -- <name-parsing utilities>=
90
   local function isVon (str)
91
      local lower = find_outside_braces(str, "%l") -- first nonbrace lowercase
×
92
      local letter = find_outside_braces(str, "%a") -- first nonbrace letter
×
93
      local bs, _, _ = find_outside_braces(str, "%{%\\(%a+)") -- \xxx
×
94
      if lower and lower <= letter and lower <= (bs or lower) then
×
95
         return true
×
96
      elseif letter and letter <= (bs or letter) then
×
97
         return false
×
98
      elseif bs then
×
99
         -- if upper_specials[command] then
100
         --   return false
101
         -- elseif lower_specials[command] then
102
         --   return true
103
         -- else
104
         -- local close_brace = find_outside_braces(str, '%}', ebs+1)
105
         lower = string.find(str, "%l") -- first nonbrace lowercase
×
106
         letter = string.find(str, "%a") -- first nonbrace letter
×
107
         return lower and lower <= letter
×
108
      -- end
109
      else
110
         return false
×
111
      end
112
   end
113

114
   function parse_name (str, inter_token)
×
115
      if string.find(str, trailing_commas) then
×
116
         SU.error("Name '%s' has one or more commas at the end", str)
×
117
      end
118
      str = string.gsub(str, trailing_commas, "")
×
119
      str = string.gsub(str, leading_white_sep, "")
×
120
      local tokens = split(str, white_comma_sep, find_outside_braces)
×
121
      local trailers = splitters(str, white_comma_sep, find_outside_braces)
×
122
      -- The string separating tokens is reduced to a single
123
      -- ``separator character.'' A comma always trumps other
124
      -- separator characters. Otherwise, if there's no comma,
125
      -- we take the first character, be it a separator or a
126
      -- space. (Patashnik considers that multiple such
127
      -- characters constitute ``silliness'' on the user's
128
      -- part.)
129
      -- <rewrite [[trailers]] to hold a single separator character each>=
130
      for i = 1, #trailers do
×
131
         local trailer = trailers[i]
×
132
         assert(string.len(trailer) > 0)
×
133
         if string.find(trailer, ",") then
×
134
            trailers[i] = ","
×
135
         else
136
            trailers[i] = string.sub(trailer, 1, 1)
×
137
         end
138
      end
139
      local commas = {} -- maps each comma to index of token the follows it
×
140
      for i, t in ipairs(trailers) do
×
141
         string.gsub(t, ",", function ()
×
142
            table.insert(commas, i + 1)
×
143
         end)
144
      end
145
      local name = {}
×
146
      -- A name has up to four parts: the most general form is
147
      -- either ``First von Last, Junior'' or ``von Last,
148
      -- First, Junior'', but various vons and Juniors can be
149
      -- omitted. The name-parsing algorithm is baroque and is
150
      -- transliterated from the original BibTeX source, but
151
      -- the principle is clear: assign the full version of
152
      -- each part to the four fields [[ff]], [[vv]], [[ll]],
153
      -- and [[jj]]; and assign an abbreviated version of each
154
      -- part to the fields [[f]], [[v]], [[l]], and [[j]].
155
      -- <parse the name tokens and set fields of [[name]]>=
156
      local first_start, first_lim, last_lim, von_start, von_lim, jr_lim
157
      -- variables mark subsequences; if start == lim, sequence is empty
158
      local n = #tokens
×
159
      -- The von name, if any, goes from the first von token to
160
      -- the last von token, except the last name is entitled
161
      -- to at least one token. So to find the limit of the von
162
      -- name, we start just before the last token and wind
163
      -- down until we find a von token or we hit the von start
164
      -- (in which latter case there is no von name).
165
      -- <local parsing functions>=
166
      local function divide_von_from_last ()
167
         von_lim = last_lim - 1
×
168
         while von_lim > von_start and not isVon(tokens[von_lim - 1]) do
×
169
            von_lim = von_lim - 1
×
170
         end
171
      end
172

173
      local commacount = #commas
×
174
      if commacount == 0 then -- first von last jr
×
175
         von_start, first_start, last_lim, jr_lim = 1, 1, n + 1, n + 1
×
176
         -- OK, here's one form.
177
         --
178
         -- <parse first von last jr>=
179
         local got_von = false
×
180
         while von_start < last_lim - 1 do
×
181
            if isVon(tokens[von_start]) then
×
182
               divide_von_from_last()
×
183
               got_von = true
×
184
               break
185
            else
186
               von_start = von_start + 1
×
187
            end
188
         end
189
         if not got_von then -- there is no von name
×
190
            while von_start > 1 and string.find(trailers[von_start - 1], sep_and_not_tie) do
×
191
               von_start = von_start - 1
×
192
            end
193
            von_lim = von_start
×
194
         end
195
         first_lim = von_start
×
196
      elseif commacount == 1 then -- von last jr, first
×
197
         von_start, last_lim, jr_lim, first_start, first_lim = 1, commas[1], commas[1], commas[1], n + 1
×
198
         divide_von_from_last()
×
199
      elseif commacount == 2 then -- von last, jr, first
×
200
         von_start, last_lim, jr_lim, first_start, first_lim = 1, commas[1], commas[2], commas[2], n + 1
×
201
         divide_von_from_last()
×
202
      else
203
         SU.error("Too many commas in name '%s'")
×
204
      end
205
      -- <set fields of name based on [[first_start]] and friends>=
206
      -- We set long and short forms together; [[ss]] is the
207
      -- long form and [[s]] is the short form.
208
      -- <definition of function [[set_name]]>=
209
      local function set_name (start, lim, long, short)
210
         if start < lim then
×
211
            -- string concatenation is quadratic, but names are short
212
            -- An abbreviated token is the first letter of a token,
213
            -- except again we have to deal with the damned specials.
214
            -- <definition of [[abbrev]], for shortening a token>=
215
            local function abbrev (token)
216
               local first_alpha, _, alpha = string.find(token, "(%a)")
×
217
               local first_brace = string.find(token, "%{%\\")
×
218
               if first_alpha and first_alpha <= (first_brace or first_alpha) then
×
219
                  return alpha
×
220
               elseif first_brace then
×
221
                  local i, _, special = string.find(token, "(%b{})", first_brace)
×
222
                  if i then
×
223
                     return special
×
224
                  else -- unbalanced braces
225
                     return string.sub(token, first_brace)
×
226
                  end
227
               else
228
                  return ""
×
229
               end
230
            end
231
            local longname = tokens[start]
×
232
            local shortname = abbrev(tokens[start])
×
233
            for i = start + 1, lim - 1 do
×
234
               if inter_token then
×
235
                  longname = longname .. inter_token .. tokens[i]
×
236
                  shortname = shortname .. inter_token .. abbrev(tokens[i])
×
237
               else
238
                  local ssep, nnext = trailers[i - 1], tokens[i]
×
239
                  local sep, next = ssep, abbrev(nnext)
×
240
                  -- Here is the default for a character between tokens:
241
                  -- a tie is the default space character between the last
242
                  -- two tokens of the name part, and between the first two
243
                  -- tokens if the first token is short enough; otherwise,
244
                  -- a space is the default.
245
                  -- <possibly adjust [[sep]] and [[ssep]] according to token position and size>=
246
                  if not string.find(sep, sep_char) then
×
247
                     if i == lim - 1 then
×
248
                        sep, ssep = "~", "~"
×
249
                     elseif i == start + 1 then
×
250
                        sep = string.len(shortname) < 3 and "~" or " "
×
251
                        ssep = string.len(longname) < 3 and "~" or " "
×
252
                     else
253
                        sep, ssep = " ", " "
×
254
                     end
255
                  end
256
                  longname = longname .. ssep .. nnext
×
257
                  shortname = shortname .. "." .. sep .. next
×
258
               end
259
            end
260
            name[long] = longname
×
261
            name[short] = shortname
×
262
         end
263
      end
264
      set_name(first_start, first_lim, "ff", "f")
×
265
      set_name(von_start, von_lim, "vv", "v")
×
266
      set_name(von_lim, last_lim, "ll", "l")
×
267
      set_name(last_lim, jr_lim, "jj", "j")
×
268
      return name
×
269
   end
270
end
271

272
-- Thanks, Norman, for the above functions!
273

274
local Bibliography
275
Bibliography = {
×
276
   CitationStyles = {
×
277
      -- luacheck: push ignore
278
      ---@diagnostic disable: undefined-global, unused-local
279
      AuthorYear = function (_ENV)
280
         return andSurnames(3), " ", year, optional(", ", cite.page)
×
281
      end,
282
      -- luacheck: pop
283
      ---@diagnostic enable: undefined-global, unused-local
284
   },
285

286
   produceCitation = function (cite, bib, style)
287
      local item = bib[cite.key]
×
288
      if not item then
×
289
         return Bibliography.Errors.UNKNOWN_REFERENCE
×
290
      end
291
      local t = Bibliography.buildEnv(cite, item.attributes, style)
×
292
      local func = setfenv and setfenv(style.CitationStyle, t) or style.CitationStyle
×
293
      return Bibliography._process(item.attributes, { func(t) })
×
294
   end,
295

296
   produceReference = function (cite, bib, style)
297
      local item = bib[cite.key]
×
298
      if not item then
×
299
         return Bibliography.Errors.UNKNOWN_REFERENCE
×
300
      end
301
      item.type = item.type:gsub("^%l", string.upper)
×
302
      if not style[item.type] then
×
303
         return Bibliography.Errors.UNKNOWN_TYPE, item.type
×
304
      end
305

306
      local t = Bibliography.buildEnv(cite, item.attributes, style)
×
307
      local func = setfenv and setfenv(style[item.type], t) or style[item.type]
×
308
      return Bibliography._process(item.attributes, { func(t) })
×
309
   end,
310

311
   buildEnv = function (cite, item, style)
312
      local t = pl.tablex.copy(getfenv and getfenv(1) or _ENV)
×
313
      t.cite = cite
×
314
      t.item = item
×
315
      for k, v in pairs(item) do
×
316
         if k:lower() == "type" then
×
317
            k = "bibtype"
×
318
         end -- HACK: don't override the type() function
319
         t[k:lower()] = v
×
320
      end
321
      return pl.tablex.update(t, style)
×
322
   end,
323

324
   _process = function (item, t, dStart, dEnd)
325
      for i = 1, #t do
×
326
         if type(t[i]) == "function" then
×
327
            t[i] = t[i](item)
×
328
         end
329
      end
330
      local res = SU.concat(t, "")
×
331
      if dStart or dEnd then
×
332
         if res ~= "" then
×
333
            return (dStart .. res .. dEnd)
×
334
         end
335
      else
336
         return res
×
337
      end
338
   end,
339

340
   Errors = {
×
341
      UNKNOWN_REFERENCE = 1,
342
      UNKNOWN_TYPE = 2,
343
   },
344

345
   Style = {
×
346
      andAuthors = function (item)
347
         local authors = namesplit(item.author)
×
348
         if #authors == 1 then
×
349
            return parse_name(authors[1]).ll
×
350
         else
351
            for i = 1, #authors do
×
352
               local author = parse_name(authors[i])
×
353
               authors[i] = author.ll .. ", " .. author.f .. "."
×
354
            end
355
            return table.concat(authors, " " .. fluent:get_message("bibliography-and") .. " ")
×
356
         end
357
      end,
358

359
      andSurnames = function (max)
360
         return function (item)
361
            local authors = namesplit(item.author)
×
362
            if #authors > max then
×
363
               return parse_name(authors[1]).ll .. " " .. fluent:get_message("bibliography-et-al")
×
364
            else
365
               for i = 1, #authors do
×
366
                  authors[i] = parse_name(authors[i]).ll
×
367
               end
368
               return Bibliography.Style.commafy(authors)
×
369
            end
370
         end
371
      end,
372

373
      pageRange = function (item)
374
         if item.pages then
×
375
            return item.pages:gsub("%-%-", "–")
×
376
         end
377
      end,
378

379
      transEditor = function (item)
380
         local r = {}
×
381
         if item.editor then
×
382
            r[#r + 1] = fluent:get_message("bibliography-edited-by")({ name = item.editor })
×
383
         end
384
         if item.translator then
×
385
            r[#r + 1] = fluent:get_message("bibliography-translated-by")({ name = item.translator })
×
386
         end
387
         if #r then
×
388
            return table.concat(r, ", ")
×
389
         end
390
         return nil
×
391
      end,
392
      quotes = function (...)
393
         local t = { ... }
×
394
         return function (item)
395
            return Bibliography._process(item, t, "“", "”")
×
396
         end
397
      end,
398
      italic = function (...)
399
         local t = { ... }
×
400
         return function (item)
401
            return Bibliography._process(item, t, "<em>", "</em>")
×
402
         end
403
      end,
404
      parens = function (...)
405
         local t = { ... }
×
406
         return function (item)
407
            return Bibliography._process(item, t, "(", ")")
×
408
         end
409
      end,
410
      optional = function (...)
411
         local t = { n = select("#", ...), ... }
×
412
         return function (item)
413
            for i = 1, t.n do
×
414
               if type(t[i]) == "function" then
×
415
                  t[i] = t[i](item)
×
416
               end
417
               if not t[i] or t[i] == "" then
×
418
                  return ""
×
419
               end
420
            end
421
            return table.concat(t, "")
×
422
         end
423
      end,
424

425
      commafy = function (t, andword) -- also stolen from nbibtex
426
         andword = andword or fluent:get_message("bibliography-and")
×
427
         if #t == 1 then
×
428
            return t[1]
×
429
         elseif #t == 2 then
×
430
            return t[1] .. " " .. andword .. " " .. t[2]
×
431
         else
432
            local last = t[#t]
×
433
            t[#t] = andword .. " " .. t[#t]
×
434
            local answer = table.concat(t, ", ")
×
435
            t[#t] = last
×
436
            return answer
×
437
         end
438
      end,
439
   },
440
}
441

442
return Bibliography
×
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