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

sile-typesetter / sile / 13980176901

20 Mar 2025 09:59PM UTC coverage: 60.236% (-8.5%) from 68.747%
13980176901

push

github

alerque
chore(release): 0.15.10

12970 of 21532 relevant lines covered (60.24%)

2843.65 hits per line

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

0.0
/packages/bibtex/support/bibparser.lua
1
local epnf = require("epnf")
×
2
local nbibtex = require("packages.bibtex.support.nbibtex")
×
3
local namesplit, parse_name = nbibtex.namesplit, nbibtex.parse_name
×
4
local isodatetime = require("packages.bibtex.support.isodatetime")
×
5

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

37
-- luacheck: push ignore
38
-- stylua: ignore start
39
---@diagnostic disable: undefined-global, unused-local, lowercase-global
40
local bibtexparser = epnf.define(function (_ENV)
×
41
   local strings = {} -- Local store for @string entries
×
42

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

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

80
local bibcompat = require("packages.bibtex.support.bibmaps")
×
81
local crossrefmap, fieldmap = bibcompat.crossrefmap, bibcompat.fieldmap
×
82
local months =
83
   { jan = 1, feb = 2, mar = 3, apr = 4, may = 5, jun = 6, jul = 7, aug = 8, sep = 9, oct = 10, nov = 11, dec = 12 }
×
84

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

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

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

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

223
   if not refs then
×
224
      return
×
225
   end
226
   for _, ref in ipairs(refs) do
×
227
      local parent = bib[ref]
×
228
      if parent then
×
229
         crossrefAndXDataResolve(bib, parent)
×
230
         fieldsInherit(parent, entry)
×
231
      else
232
         SU.warn("Unknown crossref " .. ref .. " in bibliography entry " .. entry.label)
×
233
      end
234
   end
235
end
236

237
return {
×
238
   parseBibtex = parseBibtex,
239
   crossrefAndXDataResolve = crossrefAndXDataResolve,
240
}
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