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

sile-typesetter / sile / 6713098919

31 Oct 2023 10:21PM UTC coverage: 52.831% (-21.8%) from 74.636%
6713098919

push

github

web-flow
Merge d0a2a1ee9 into b185d4972

45 of 45 new or added lines in 3 files covered. (100.0%)

8173 of 15470 relevant lines covered (52.83%)

6562.28 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

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

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

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

28
  local len = string.len(str)
×
29
  local t = { }
×
30
  local insert = table.insert
×
31
  local i, j = 1, true
×
32
  local k
33
  while j and i <= len + 1 do
×
34
    j, k = find(str, pat, i)
×
35
    if j then
×
36
      insert(t, string.sub(str, i, j-1))
×
37
      i = k + 1
×
38
    else
39
      insert(t, string.sub(str, i))
×
40
    end
41
  end
42
  return t
×
43
end
44

45
local function splitters(str, pat, find) --- return list of separators
46
  find = find or string.find -- could be find_outside_braces
×
47
  local t = { }
×
48
  local insert = table.insert
×
49
  local j, k = find(str, pat, 1)
×
50
  while j do
×
51
    insert(t, string.sub(str, j, k))
×
52
    j, k = find(str, pat, k+1)
×
53
  end
54
  return t
×
55
end
56

57
local function namesplit(str)
58
  local t = split(str, '%s+[aA][nN][dD]%s+', find_outside_braces)
×
59
  local i = 2
×
60
  while i <= #t do
×
61
    while string.find(t[i], '^[aA][nN][dD]%s+') do
×
62
      t[i] = string.gsub(t[i], '^[aA][nN][dD]%s+', '')
×
63
      table.insert(t, i, '')
×
64
      i = i + 1
×
65
    end
66
    i = i + 1
×
67
  end
68
  return t
×
69
end
70

71
local sep_and_not_tie = '%-'
×
72
local sep_chars = sep_and_not_tie .. '%~'
×
73

74
local parse_name
75
do
76
  local white_sep         = '[' .. sep_chars .. '%s]+'
×
77
  local white_comma_sep   = '[' .. sep_chars .. '%s%,]+'
×
78
  local trailing_commas   = '(,[' .. sep_chars .. '%s%,]*)$'
×
79
  local sep_char          = '[' .. sep_chars .. ']'
×
80
  local leading_white_sep = '^' .. white_sep
×
81

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

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

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

265
--- Thanks, Norman, for the above functions!
266

267
local Bibliography
268
Bibliography = {
×
269
  CitationStyles = {
×
270
    -- luacheck: push ignore
271
    ---@diagnostic disable: undefined-global, unused-local
272
    AuthorYear = function(_ENV)
273
      return andSurnames(3), " ", year, optional(", ", cite.page)
×
274
    end
275
    -- luacheck: pop
276
    ---@diagnostic enable: undefined-global, unused-local
277
  },
278

279
  produceCitation = function (cite, bib, style)
280
    local item = bib[cite.key]
×
281
    if not item then
×
282
      return Bibliography.Errors.UNKNOWN_REFERENCE
×
283
    end
284
    local t = Bibliography.buildEnv(cite, item.attributes, style)
×
285
    local func = setfenv and setfenv(style.CitationStyle, t) or style.CitationStyle
×
286
    return Bibliography._process(item.attributes, {func(t)})
×
287
  end,
288

289
  produceReference = function (cite, bib, style)
290
    local item = bib[cite.key]
×
291
    if not item then
×
292
      return Bibliography.Errors.UNKNOWN_REFERENCE
×
293
    end
294
    item.type = item.type:gsub("^%l", string.upper)
×
295
    if not style[item.type] then
×
296
      return Bibliography.Errors.UNKNOWN_TYPE, item.type
×
297
    end
298

299
    local t = Bibliography.buildEnv(cite, item.attributes, style)
×
300
    local func = setfenv and setfenv(style[item.type], t) or style[item.type]
×
301
    return Bibliography._process(item.attributes, {func(t)})
×
302
  end,
303

304
  buildEnv = function (cite,item, style)
305
    local t = pl.tablex.copy(getfenv and getfenv(1) or _ENV)
×
306
    t.cite = cite
×
307
    t.item = item
×
308
    for k,v in pairs(item) do
×
309
      if k:lower() == "type" then k = "bibtype" end -- HACK: don't override the type() function
×
310
      t[k:lower()] = v
×
311
    end
312
    return pl.tablex.update(t, style)
×
313
  end,
314

315
  _process = function (item, t, dStart, dEnd)
316
    for i = 1,#t do
×
317
      if type(t[i]) == "function" then
×
318
        t[i] = t[i](item)
×
319
      end
320
    end
321
    local res = SU.concat(t,"")
×
322
    if dStart or dEnd then
×
323
      if res ~= "" then return (dStart .. res .. dEnd) end
×
324
    else
325
      return res
×
326
    end
327
  end,
328

329
  Errors = {
×
330
    UNKNOWN_REFERENCE = 1,
331
    UNKNOWN_TYPE = 2,
332
  },
333

334
  Style = {
×
335
    andAuthors = function(item)
336
      local authors = namesplit(item.author)
×
337
      if #authors == 1 then
×
338
        return parse_name(authors[1]).ll
×
339
      else
340
        for i = 1,#authors do
×
341
          local author = parse_name(authors[i])
×
342
          authors[i] = author.ll.. ", "..author.f.."."
×
343
        end
344
        return table.concat(authors, " ".. fluent:get_message("bibliography-and") .. " ")
×
345
      end
346
    end,
347

348
    andSurnames = function (max)
349
      return function(item)
350
        local authors = namesplit(item.author)
×
351
        if #authors > max then
×
352
          return parse_name(authors[1]).ll .. " " .. fluent:get_message("bibliography-et-al")
×
353
        else
354
          for i = 1,#authors do authors[i] = parse_name(authors[i]).ll end
×
355
          return Bibliography.Style.commafy(authors)
×
356
        end
357
      end
358
    end,
359

360
    pageRange = function(item)
361
      if item.pages then
×
362
        return item.pages:gsub("%-%-", "–")
×
363
      end
364
    end,
365

366
    transEditor = function(item)
367
      local r = {}
×
368
      if item.editor then
×
369
        r[#r+1] = fluent:get_message("bibliography-edited-by")({ name = item.editor })
×
370
      end
371
      if item.translator then
×
372
        r[#r+1] = fluent:get_message("bibliography-translated-by")({ name = item.translator })
×
373
      end
374
      if #r then return table.concat(r, ", ") end
×
375
      return nil
×
376
    end,
377
    quotes = function (...)
378
      local t = {...}
×
379
      return function(item)
380
        return Bibliography._process(item, t, "“", "”")
×
381
      end
382
    end,
383
    italic = function (...)
384
      local t = {...}
×
385
      return function(item)
386
        return Bibliography._process(item, t, "<em>", "</em>")
×
387
      end
388
    end,
389
    parens = function (...)
390
      local t = {...}
×
391
      return function(item)
392
        return Bibliography._process(item, t, "(", ")")
×
393
      end
394
    end,
395
    optional = function(...)
396
      local t = {n=select('#', ...), ...}
×
397
      return function(item)
398
        for i = 1,t.n do
×
399
          if type(t[i]) == "function" then t[i] = t[i](item) end
×
400
          if not t[i] or t[i] == "" then return "" end
×
401
        end
402
        return table.concat(t, "")
×
403
      end
404
    end,
405

406
    commafy = function (t, andword) -- also stolen from nbibtex
407
      andword = andword or fluent:get_message("bibliography-and")
×
408
      if #t == 1 then
×
409
        return t[1]
×
410
      elseif #t == 2 then
×
411
        return t[1] .. ' ' .. andword .. ' ' .. t[2]
×
412
      else
413
        local last = t[#t]
×
414
        t[#t] = andword .. ' ' .. t[#t]
×
415
        local answer = table.concat(t, ', ')
×
416
        t[#t] = last
×
417
        return answer
×
418
      end
419
    end
420
  }
421
}
422

423
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