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

sile-typesetter / sile / 10621606353

29 Aug 2024 07:43PM UTC coverage: 66.23% (+3.6%) from 62.644%
10621606353

push

github

alerque
Merge tag 'v0.15.5' into develop

13 of 289 new or added lines in 17 files covered. (4.5%)

403 existing lines in 59 files now uncovered.

11585 of 17492 relevant lines covered (66.23%)

5713.06 hits per line

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

0.0
/packages/bibtex/init.lua
1
local base = require("packages.base")
×
2

3
local package = pl.class(base)
×
4
package._name = "bibtex"
×
5

6
local epnf = require("epnf")
×
NEW
7
local nbibtex = require("packages.bibtex.support.nbibtex")
×
NEW
8
local namesplit, parse_name = nbibtex.namesplit, nbibtex.parse_name
×
NEW
9
local isodatetime = require("packages.bibtex.support.isodatetime")
×
10

11
local Bibliography
12

13
local nbsp = luautf8.char(0x00A0)
×
14
local function sanitize (str)
15
   local s = str
×
16
      -- TeX special characters:
17
      -- Backslash-escaped tilde is a tilde,
18
      -- but standalone tilde is a non-breaking space
19
      :gsub(
×
20
         "(.?)~",
21
         function (prev)
22
            if prev == "\\" then
×
23
               return "~"
×
24
            end
25
            return prev .. nbsp
×
26
         end
27
      )
28
      -- Other backslash-escaped characters are skipped
29
      -- TODO FIXME:
30
      -- This ok for \", \& etc. which we want to unescape,
31
      -- BUT what should we do with other TeX-like commands?
32
      :gsub(
×
33
         "\\",
34
         ""
35
      )
36
      -- We will wrap the content in <sile> tags so we need to XML-escape
37
      -- the input.
38
      :gsub("&", "&amp;")
×
39
      :gsub("<", "&lt;")
×
40
      :gsub(">", "&gt;")
×
41
   return s
×
42
end
43

44
-- luacheck: push ignore
45
-- stylua: ignore start
46
---@diagnostic disable: undefined-global, unused-local, lowercase-global
47
local bibtexparser = epnf.define(function (_ENV)
×
48
   local strings = {} -- Local store for @string entries
×
49

50
   local identifier = (SILE.parserBits.identifier + S":-")^1
×
51
   local balanced = C{ "{" * P" "^0 * C(((1 - S"{}") + V(1))^0) * "}" } / function (...) local t={...}; return t[2] end
×
52
   local quoted = C( P'"' * C(((1 - S'"\r\n\f\\') + (P'\\' * 1)) ^ 0) * '"' ) / function (...) local t={...}; return t[2] end
×
53
   local _ = WS^0
×
54
   local sep = S",;" * _
×
55
   local myID = C(identifier)
×
56
   local myStrID = myID / function (t) return strings[t] or t end
×
57
   local myTag = C(identifier) / function (t) return t:lower() end
×
58
   local pieces = balanced + quoted + myStrID
×
59
   local value = Ct(pieces * (WS * P("#") * WS * pieces)^0)
×
60
      / function (t) return table.concat(t) end / sanitize
×
61
   local pair = myTag * _ * "=" * _ * value * _ * sep^-1
×
62
      / function (...) local t= {...}; return t[1], t[#t] end
×
63
   local list = Cf(Ct("") * pair^0, rawset)
×
64
   local skippedType = Cmt(R("az", "AZ")^1, function(_, _, tag)
×
65
      -- ignore both @comment and @preamble
66
      local t = tag:lower()
×
67
      return t == "comment" or t == "preamble"
×
68
   end)
69

70
   START "document"
71
   document = (V"skipped" -- order important: skipped (@comment, @preamble) must be first
×
72
      + V"stringblock" -- order important: @string must be before @entry
×
73
      + V"entry")^1
×
74
      * (-1 + E("Unexpected character at end of input"))
×
75
   skipped  = WS + (V"blockskipped" + (1 - P"@")^1 ) / ""
×
76
   blockskipped = (P("@") * skippedType) + balanced / ""
×
77
   stringblock = Ct( P("@string") * _ * P("{") * pair * _ * P("}") * _ )
×
78
       / function (t)
×
79
          strings[t[1]] = t[2]
×
80
          return t end
×
81
   entry = Ct( P("@") * Cg(myTag, "type") * _ * P("{") * _ * Cg(myID, "label") * _ * sep * list * P("}") * _ )
×
82
end)
83
-- luacheck: pop
84
-- stylua: ignore end
85
---@diagnostic enable: undefined-global, unused-local, lowercase-global
86

NEW
87
local bibcompat = require("packages.bibtex.support.bibmaps")
×
88
local crossrefmap, fieldmap = bibcompat.crossrefmap, bibcompat.fieldmap
×
89
local months =
NEW
90
   { jan = 1, feb = 2, mar = 3, apr = 4, may = 5, jun = 6, jul = 7, aug = 8, sep = 9, oct = 10, nov = 11, dec = 12 }
×
91

92
local function consolidateEntry (entry, label)
93
   local consolidated = {}
×
94
   -- BibLaTeX aliases for legacy BibTeX fields
95
   for field, value in pairs(entry.attributes) do
×
96
      consolidated[field] = value
×
97
      local alias = fieldmap[field]
×
98
      if alias then
×
99
         if entry.attributes[alias] then
×
100
            SU.warn("Duplicate field '" .. field .. "' and alias '" .. alias .. "' in entry '" .. label .. "'")
×
101
         else
102
            consolidated[alias] = value
×
103
         end
104
      end
105
   end
106
   -- Names field split and parsed
NEW
107
   for _, field in ipairs({ "author", "editor", "translator", "shortauthor", "shorteditor", "holder" }) do
×
NEW
108
      if consolidated[field] then
×
109
         -- FIXME Check our corporate names behave, we are probably bad currently
110
         -- with nested braces !!!
111
         -- See biblatex manual v3.20 §2.3.3 Name Lists
112
         -- e.g. editor = {{National Aeronautics and Space Administration} and Doe, John}
NEW
113
         local names = namesplit(consolidated[field])
×
NEW
114
         for i = 1, #names do
×
NEW
115
            names[i] = parse_name(names[i])
×
116
         end
NEW
117
         consolidated[field] = names
×
118
      end
119
   end
120
   -- Month field in either number or string (3-letter code)
NEW
121
   if consolidated.month then
×
NEW
122
      local month = tonumber(consolidated.month) or months[consolidated.month:lower()]
×
NEW
123
      if month and (month >= 1 and month <= 12) then
×
NEW
124
         consolidated.month = month
×
125
      else
NEW
126
         SU.warn("Unrecognized month skipped in entry '" .. label .. "'")
×
NEW
127
         consolidated.month = nil
×
128
      end
129
   end
130
   -- Extended date fields
NEW
131
   for _, field in ipairs({ "date", "origdate", "eventdate", "urldate" }) do
×
NEW
132
      if consolidated[field] then
×
NEW
133
         local dt = isodatetime(consolidated[field])
×
NEW
134
         if dt then
×
NEW
135
            consolidated[field] = dt
×
136
         else
NEW
137
            SU.warn("Invalid '" .. field .. "' skipped in entry '" .. label .. "'")
×
NEW
138
            consolidated[field] = nil
×
139
         end
140
      end
141
   end
142
   entry.attributes = consolidated
×
143
   return entry
×
144
end
145

146
--- Parse a BibTeX file and populate a bibliography table.
147
-- @tparam string fn Filename
148
-- @tparam table biblio Table of entries
149
local function parseBibtex (fn, biblio)
150
   fn = SILE.resolveFile(fn) or SU.error("Unable to resolve Bibtex file " .. fn)
×
151
   local fh, e = io.open(fn)
×
152
   if e then
×
153
      SU.error("Error reading bibliography file: " .. e)
×
154
   end
155
   local doc = fh:read("*all")
×
156
   local t = epnf.parsestring(bibtexparser, doc)
×
157
   if not t or not t[1] or t.id ~= "document" then
×
158
      SU.error("Error parsing bibtex")
×
159
   end
160
   for i = 1, #t do
×
161
      if t[i].id == "entry" then
×
162
         local ent = t[i][1]
×
163
         local entry = { type = ent.type, attributes = ent[1] }
×
164
         if biblio[ent.label] then
×
165
            SU.warn("Duplicate entry key '" .. ent.label .. "', picking the last one")
×
166
         end
167
         biblio[ent.label] = consolidateEntry(entry, ent.label)
×
168
      end
169
   end
170
end
171

172
--- Copy fields from the parent entry to the child entry.
173
-- BibLaTeX/Biber have a complex inheritance system for fields.
174
-- This implementation is more naive, but should be sufficient for reasonable
175
-- use cases.
176
-- @tparam table parent Parent entry
177
-- @tparam table entry Child entry
178
local function fieldsInherit (parent, entry)
179
   local map = crossrefmap[parent.type] and crossrefmap[parent.type][entry.type]
×
180
   if not map then
×
181
      -- @xdata and any other unknown types: inherit all missing fields
182
      for field, value in pairs(parent.attributes) do
×
183
         if not entry.attributes[field] then
×
184
            entry.attributes[field] = value
×
185
         end
186
      end
187
      return -- done
×
188
   end
189
   for field, value in pairs(parent.attributes) do
×
190
      if map[field] == nil and not entry.attributes[field] then
×
191
         entry.attributes[field] = value
×
192
      end
193
      for childfield, parentfield in pairs(map) do
×
194
         if parentfield and not entry.attributes[parentfield] then
×
195
            entry.attributes[parentfield] = parent.attributes[childfield]
×
196
         end
197
      end
198
   end
199
end
200

201
--- Resolve the 'crossref' and 'xdata' fields on a bibliography entry.
202
-- (Supplementing the entry with the attributes of the parent entry.)
203
-- Once resolved recursively, the crossref and xdata fields are removed
204
-- from the entry.
205
-- So this is intended to be called at first use of the entry, and have no
206
-- effect on subsequent uses: BibTeX does seem to mandate cross references
207
-- to be defined before the entry that uses it, or even in the same bibliography
208
-- file.
209
-- Implementation note:
210
-- We are not here to check the consistency of the BibTeX file, so there is
211
-- no check that xdata refers only to @xdata entries
212
-- Removing the crossref field implies we won't track its use and implicitly
213
-- cite referenced entries in the bibliography over a certain threshold.
214
-- @tparam table bib Bibliography
215
-- @tparam table entry Bibliography entry
216
local function crossrefAndXDataResolve (bib, entry)
217
   local refs
UNCOV
218
   local xdata = entry.attributes.xdata
×
UNCOV
219
   if xdata then
×
220
      refs = xdata and pl.stringx.split(xdata, ",")
×
221
      entry.attributes.xdata = nil
×
222
   end
223
   local crossref = entry.attributes.crossref
×
UNCOV
224
   if crossref then
×
225
      refs = refs or {}
×
226
      table.insert(refs, crossref)
×
227
      entry.attributes.crossref = nil
×
228
   end
229

UNCOV
230
   if not refs then
×
UNCOV
231
      return
×
232
   end
233
   for _, ref in ipairs(refs) do
×
UNCOV
234
      local parent = bib[ref]
×
235
      if parent then
×
236
         crossrefAndXDataResolve(bib, parent)
×
237
         fieldsInherit(parent, entry)
×
238
      else
239
         SU.warn("Unknown crossref " .. ref .. " in bibliography entry " .. entry.label)
×
240
      end
241
   end
242
end
243

UNCOV
244
function package:_init ()
×
UNCOV
245
   base._init(self)
×
246
   SILE.scratch.bibtex = { bib = {} }
×
247
   Bibliography = require("packages.bibtex.bibliography")
×
248
end
249

UNCOV
250
function package.declareSettings (_)
×
UNCOV
251
   SILE.settings:declare({
×
252
      parameter = "bibtex.style",
253
      type = "string",
254
      default = "chicago",
255
      help = "BibTeX style",
256
   })
257
end
258

UNCOV
259
function package:registerCommands ()
×
UNCOV
260
   self:registerCommand("loadbibliography", function (options, _)
×
261
      local file = SU.required(options, "file", "loadbibliography")
×
262
      parseBibtex(file, SILE.scratch.bibtex.bib)
×
263
   end)
264

UNCOV
265
   self:registerCommand("bibstyle", function (_, _)
×
UNCOV
266
      SU.deprecated("\\bibstyle", "\\set[parameter=bibtex.style]", "0.13.2", "0.14.0")
×
267
   end)
268

UNCOV
269
   self:registerCommand("cite", function (options, content)
×
UNCOV
270
      if not options.key then
×
271
         options.key = SU.ast.contentToString(content)
×
272
      end
273
      local entry = SILE.scratch.bibtex.bib[options.key]
×
UNCOV
274
      if not entry then
×
275
         SU.warn("Unknown reference in citation " .. options.key)
×
276
         return
×
277
      end
278
      if entry.type == "xdata" then
×
UNCOV
279
         SU.warn("Skipped citation of @xdata entry " .. options.key)
×
280
         return
×
281
      end
282
      crossrefAndXDataResolve(SILE.scratch.bibtex.bib, entry)
×
UNCOV
283
      local style = SILE.settings:get("bibtex.style")
×
284
      local bibstyle = require("packages.bibtex.styles." .. style)
×
285
      local cite = Bibliography.produceCitation(options, SILE.scratch.bibtex.bib, bibstyle)
×
286
      SILE.processString(("<sile>%s</sile>"):format(cite), "xml")
×
287
   end)
288

UNCOV
289
   self:registerCommand("reference", function (options, content)
×
UNCOV
290
      if not options.key then
×
291
         options.key = SU.ast.contentToString(content)
×
292
      end
293
      local entry = SILE.scratch.bibtex.bib[options.key]
×
UNCOV
294
      if not entry then
×
295
         SU.warn("Unknown reference in citation " .. options.key)
×
296
         return
×
297
      end
298
      if entry.type == "xdata" then
×
UNCOV
299
         SU.warn("Skipped citation of @xdata entry " .. options.key)
×
300
         return
×
301
      end
302
      crossrefAndXDataResolve(SILE.scratch.bibtex.bib, entry)
×
UNCOV
303
      local style = SILE.settings:get("bibtex.style")
×
304
      local bibstyle = require("packages.bibtex.styles." .. style)
×
305
      local cite, err = Bibliography.produceReference(options, SILE.scratch.bibtex.bib, bibstyle)
×
306
      if cite == Bibliography.Errors.UNKNOWN_TYPE then
×
307
         SU.warn("Unknown type @" .. err .. " in citation for reference " .. options.key)
×
308
         return
×
309
      end
310
      SILE.processString(("<sile>%s</sile>"):format(cite), "xml")
×
311
   end)
312
end
313

314
package.documentation = [[
315
\begin{document}
316
BibTeX is a citation management system.
317
It was originally designed for TeX but has since been integrated into a variety of situations.
318

319
This experimental package allows SILE to read and process BibTeX \code{.bib} files and output citations and full text references.
320
(It doesn’t currently produce full bibliography listings.)
321

322
To load a BibTeX file, issue the command \autodoc:command{\loadbibliography[file=<whatever.bib>]}
323

324
To produce an inline citation, call \autodoc:command{\cite{<key>}}, which will typeset something like “Jones 1982”.
325
If you want to cite a particular page number, use \autodoc:command{\cite[page=22]{<key>}}.
326

327
To produce a full reference, use \autodoc:command{\reference{<key>}}.
328

329
Currently, the only supported bibliography style is Chicago referencing, but other styles should be easy to implement.
330
Adapt \code{packages/bibtex/styles/chicago.lua} as necessary.
331

332
\smallskip
333
\noindent
334
\em{Notes on the supported BibTeX syntax}
335
\novbreak
336

337
\indent
338
The BibTeX file format is a plain text format for bibliographies.
339

340
The \code{@type\{…\}} syntax is used to specify an entry, where \code{type} is the type of the entry, and is case-insensitive.
341
Any content outside entries is ignored.
342

343
The \code{@preamble} and \code{@comment} special entries are ignored.
344
The former is specific to TeX-based systems, and the latter is a comment (everything between the balanced braces is ignored).
345

346
The \code{@string\{key=value\}} special entry is used to define a string or “abbreviation,” for use in other subsequent entries.
347

348
The \code{@xdata} entry is used to define an entry that can be used as a reference in other entries.
349
Such entries are not printed in the bibliography.
350
Normally, they cannot be cited directly.
351
In this implementation, a warning is raised if they are; but as they have no known type, their formatting is not well-defined, and might not be meaningful.
352

353
Regular bibliography entries have the following syntax:
354

355
\begin[type=autodoc:codeblock]{raw}
356
@type{key,
357
  field1 = value1,
358
  field2 = value2,
359
  …
360
}
361
\end{raw}
362

363
The entry key is a unique identifier for the entry, and is case-sensitive.
364
Entries consist of fields, which are key-value pairs.
365
The field names are case-insensitive.
366
Spaces and line breaks are not important, except for readability.
367
On the contrary, commas are compulsory between any two fields of an entry.
368

369
String values shall be enclosed in either double quotes or curly braces.
370
The latter allows using quotes inside the string, while the former does not without escaping them with a backslash.
371

372
When string values are not enclosed in quotes or braces, they must not contain any whitespace characters.
373
The value is then considered to be a reference to an abbreviation previously defined in a \code{@string} entry.
374
If no such abbreviation is found, the value is considered to be a string literal.
375
(This allows a decent fallback for fields where curly braces or double quotes could historically be omitted, such as numerical values, and one-word strings.)
376

377
String values are assumed to be in the UTF-8 encoding, and shall not contain (La)TeX commands.
378
Special character sequences from TeX (such as \code{`} assumed to be an opening quote) are not supported.
379
There are exceptions to this rule. Notably, the \code{~} character can be used to represent a non-breaking space (when not backslash-escaped), and the \code{\\&} sequence is accepted (though this implementation does not mandate escaping ampersands).
380

381
Values can also be composed by concatenating strings, using the \code{#} character.
382

383
Besides using string references, entries have two other \em{parent-child} inheritance mechanisms allowing to reuse fields from other entries, without repeating them: the \code{crossref} and \code{xdata} fields.
384

385
The \code{crossref} field is used to reference another entry by its key.
386
The \code{xdata} field accepts a comma-separated list of keys of entries that are to be inherited.
387

388
Some BibTeX implementations automatically include entries referenced with the \code{crossref} field in the bibliography, when a certain threshold is met.
389
This implementation does not do that.
390

391
Depending on the types of the parent and child entries, the child entry may inherit some or all fields from the parent entry, and some inherited fields may be reassigned in the child entry.
392
For instance, the \code{title} in a \code{@collection} entry is inherited as the \code{booktitle} field in a \code{@incollection} child entry.
393
Some BibTeX implementations allow configuring the data inheritance behavior, but this implementation does not.
394
It is also currently quite limited on the fields that are reassigned, and only provides a subset of the mappings defined in the BibLaTeX manual, appendix B.
395

396
Here is an example of a BibTeX file showing some of the abovementioned features:
397

398
\begin[type=autodoc:codeblock]{raw}
399
@string{JIT = "Journal of Interesting Things"}
400
...
401
This text is ignored
402
...
403
@xdata{jit-vol1-iss2,
404
  journal = JIT # { (JIT)},
405
  year    = {2020},
406
  month   = {jan},
407
  volume  = {1},
408
  number  = {2},
409
}
410
@article{my-article,
411
  author  = {Doe, John and Smith, Jane}
412
  title   = {Theories & Practices},
413
  xdata   = {jit-1-2},
414
  pages   = {100--200},
415
}
416
\end{raw}
417

418
Some fields have a special syntax.
419
The \code{author}, \code{editor} and \code{translator} fields accept a list of names, separated by the keyword \code{and}.
420
The legacy \code{month} field accepts a three-letter abbreviation for the month in English, or a number from 1 to 12.
421
The more powerful \code{date} field accepts a date-time following the ISO 8601-2 Extended Date/Time Format specification level 1 (such as \code{YYYY-MM-DD}, or a date range \code{YYYY-MM-DD/YYYY-MM-DD}, and more).
422
\end{document}
UNCOV
423
]]
×
424

425
return package
×
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