pandoc-jats-ruby 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "pandoc/jats/ruby"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,193 @@
1
+ <?xml version="1.0" encoding="utf-8" ?>
2
+ <!DOCTYPE article PUBLIC "-//NLM//DTD JATS (Z39.96) Journal Publishing DTD v1.0 20120330//EN"
3
+ "JATS-journalpublishing1.dtd">
4
+ $if(article.type)$
5
+ <article xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink" dtd-version="1.0" article-type="$article.type$">
6
+ $else$
7
+ <article xmlns:mml="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink" dtd-version="1.0" article-type="other">
8
+ $endif$
9
+ <front>
10
+ <journal-meta>
11
+ $if(journal.publisher-id)$
12
+ <journal-id journal-id-type="publisher-id">$journal.publisher-id$</journal-id>
13
+ $endif$
14
+ $if(journal.nlm-ta)$
15
+ <journal-id journal-id-type="nlm-ta">$journal.nlm-ta$</journal-id>
16
+ $endif$
17
+ $if(journal.pmc)$
18
+ <journal-id journal-id-type="pmc">$journal.pmc$</journal-id>
19
+ $endif$
20
+ <journal-title-group>
21
+ $if(journal.title)$
22
+ <journal-title>$journal.title$</journal-title>
23
+ $endif$
24
+ $if(journal.abbrev-title)$
25
+ <abbrev-journal-title>$journal.abbrev-title$</abbrev-journal-title>
26
+ $endif$
27
+ </journal-title-group>
28
+ $if(journal.pissn)$
29
+ <issn pub-type="ppub">$journal.pissn$</issn>
30
+ $endif$
31
+ $if(journal.eissn)$
32
+ <issn pub-type="epub">$journal.eissn$</issn>
33
+ $endif$
34
+ <publisher>
35
+ <publisher-name>$journal.publisher-name$</publisher-name>
36
+ $if(journal.publisher-loc)$
37
+ <publisher-loc>$journal.publisher-loc$</publisher-loc>
38
+ $endif$
39
+ </publisher>
40
+ </journal-meta>
41
+ <article-meta>
42
+ $if(article.publisher-id)$
43
+ <article-id pub-id-type="publisher-id">$article.publisher-id$</article-id>
44
+ $endif$
45
+ $if(article.doi)$
46
+ <article-id pub-id-type="doi">$article.doi$</article-id>
47
+ $endif$
48
+ $if(article.pmid)$
49
+ <article-id pub-id-type="pmid">$article.pmid$</article-id>
50
+ $endif$
51
+ $if(article.pmcid)$
52
+ <article-id pub-id-type="pmcid">$article.pmcid$</article-id>
53
+ $endif$
54
+ $if(article.art-access-id)$
55
+ <article-id pub-id-type="art-access-id">$article.art-access-id$</article-id>
56
+ $endif$
57
+ $if(article.heading)$
58
+ <article-categories>
59
+ <subj-group subj-group-type="heading">
60
+ <subject>$article.heading$</subject>
61
+ </subj-group>
62
+ $if(article.categories)$
63
+ <subj-group subj-group-type="categories">
64
+ $for(article.categories)$
65
+ <subject>$article.categories$</subject>
66
+ $endfor$
67
+ </subj-group>
68
+ $endif$
69
+ </article-categories>
70
+ $endif$
71
+ $if(title)$
72
+ <title-group>
73
+ <article-title>$title$</article-title>
74
+ </title-group>
75
+ $endif$
76
+ $if(author)$
77
+ <contrib-group>
78
+ $for(author)$
79
+ <contrib contrib-type="author">
80
+ $if(author.orcid)$
81
+ <contrib-id contrib-id-type="orcid">$author.orcid$</contrib-id>
82
+ $endif$
83
+ <name>
84
+ $if(author.surname)$
85
+ <surname>$author.surname$</surname>
86
+ <given-names>$author.given-names$</given-names>
87
+ $else$
88
+ <string-name>$author$</string-name>
89
+ $endif$
90
+ </name>
91
+ $if(author.email)$
92
+ <email>$author.email$</email>
93
+ $endif$
94
+ $if(author.aff-id)$
95
+ <xref ref-type="aff" rid="aff-$contrib.aff-id$"/>
96
+ $endif$
97
+ $if(author.cor-id)$
98
+ <xref ref-type="corresp" rid="cor-$author.cor-id$"><sup>*</sup></xref>
99
+ $endif$
100
+ </contrib>
101
+ $endfor$
102
+ </contrib-group>
103
+ $endif$
104
+ $if(article.author-notes)$
105
+ <author-notes>
106
+ $if(article.author-notes.corresp)$
107
+ $for(article.author-notes.corresp)$
108
+ <corresp id="cor-$article.author-notes.corresp.id$">* E-mail: <email>$article.author-notes.corresp.email$</email></corresp>
109
+ $endfor$
110
+ $endif$
111
+ $if(article.author-notes.conflict)$
112
+ <fn fn-type="conflict"><p>$article.author-notes.conflict$</p></fn>
113
+ $endif$
114
+ $if(article.author-notes.con)$
115
+ <fn fn-type="con"><p>$article.author-notes.con$</p></fn>
116
+ $endif$
117
+ </author-notes>
118
+ $endif$
119
+ $if(date)$
120
+ $if(date.iso-8601)$
121
+ <pub-date pub-type="epub" iso-8601-date="$date.iso-8601$">
122
+ $else$
123
+ <pub-date pub-type="epub">
124
+ $endif$
125
+ $if(date.day)$
126
+ <day>$pub-date.day$</day>
127
+ $endif$
128
+ $if(date.month)$
129
+ <month>$pub-date.month$</month>
130
+ $endif$
131
+ $if(date.year)$
132
+ <year>$pub-date.year$</year>
133
+ $else$
134
+ <string-date>$date$</string-date>
135
+ $endif$
136
+ </pub-date>
137
+ $endif$
138
+ $if(article.volume)$
139
+ <volume>$article.volume$</volume>
140
+ $endif$
141
+ $if(article.issue)$
142
+ <issue>$article.issue$</issue>
143
+ $endif$
144
+ $if(article.fpage)$
145
+ <fpage>$article.fpage$</fpage>
146
+ $endif$
147
+ $if(article.lpage)$
148
+ <lpage>$article.lpage$</lpage>
149
+ $endif$
150
+ $if(article.elocation-id)$
151
+ <elocation-id>$article.elocation-id$</elocation-id>
152
+ $endif$
153
+ $if(history)$
154
+ <history>
155
+ </history>
156
+ $endif$
157
+ $if(copyright)$
158
+ <permissions>
159
+ $if(copyright.statement)$
160
+ <copyright-statement>$copyright.statement$</copyright-statement>
161
+ $endif$
162
+ $if(copyright.year)$
163
+ <copyright-year>$copyright.year$</copyright-year>
164
+ $endif$
165
+ $if(copyright.holder)$
166
+ <copyright-holder>$copyright.holder$</copyright-holder>
167
+ $endif$
168
+ $if(copyright.text)$
169
+ <license license-type="$copyright.type$" xlink:href="$copyright.link$">
170
+ <license-p>$copyright.text$</license-p>
171
+ </license>
172
+ </permissions>
173
+ $endif$
174
+ $endif$
175
+ $if(tags)$
176
+ <kwd-group kwd-group-type="author">
177
+ $for(tags)$
178
+ <kwd>$tags$</kwd>
179
+ $endfor$
180
+ </kwd-group>
181
+ $endif$
182
+ $if(article.funding-statement)$
183
+ <funding-group>
184
+ <funding-statement>$article.funding-statement$</funding-statement>
185
+ </funding-group>
186
+ $endif$
187
+ </article-meta>
188
+ $if(notes)$
189
+ <notes>$notes$</notes>
190
+ $endif$
191
+ </front>
192
+ $body$
193
+ </article>
@@ -0,0 +1,662 @@
1
+ -- This is a JATS custom writer for pandoc. It produces output
2
+ -- that tries to conform to the JATS 1.0 specification
3
+ -- http://jats.nlm.nih.gov/archiving/tag-library/1.0/index.html
4
+ --
5
+ -- Invoke with: pandoc -t jats.lua
6
+ --
7
+ -- Note: you need not have lua installed on your system to use this
8
+ -- custom writer. However, if you do have lua installed, you can
9
+ -- use it to test changes to the script. 'lua JATS.lua' will
10
+ -- produce informative error messages if your code contains
11
+ -- syntax errors.
12
+ --
13
+ -- Released under the GPL, version 2 or greater. See LICENSE for more info.
14
+
15
+ -- Tables to store metadata, headers, sections, back sections, references, figures and footnotes
16
+ local meta = {}
17
+ local headers = {}
18
+ local sections = {}
19
+ local back = {}
20
+ local references = {}
21
+ local figures = {}
22
+
23
+ -- This function is called once for the whole document. Parameters:
24
+ -- body is a string, metadata is a table, variables is a table.
25
+ -- This gives you a fragment. You could use the metadata table to
26
+ -- fill variables in a custom lua template. Or, pass `--template=...`
27
+ -- to pandoc, and pandoc will do the template processing as
28
+ -- usual.
29
+ function Doc(body, metadata, variables)
30
+ meta = metadata or {}
31
+
32
+ -- if document doesn't start with section, add top-level section without title
33
+ if string.sub(body, 1, 6) ~= '</sec>' then
34
+ body = Header(1, '') .. '\n' .. body
35
+ end
36
+
37
+ -- strip closing section tag from beginning, add to end of document
38
+ body = string.sub(body, 7) .. '</sec>'
39
+
40
+ -- parse sections, turn body into table of sections
41
+ for lev, title, content in string.gmatch(body, '<sec.-lev="(.-)".->%s<title>(.-)</title>(.-)</sec>') do
42
+ attr = section_helper(tonumber(lev), content, title)
43
+ end
44
+
45
+ body = xml('body', '\n' .. table.concat(sections, '\n') .. '\n')
46
+
47
+ if #back > 0 then
48
+ body = body .. '\n' .. xml('back', '\n' .. table.concat(back, '\n'))
49
+ end
50
+
51
+ return body
52
+ end
53
+
54
+ -- XML character entity escaping and unescaping
55
+ function escape(s)
56
+ local map = { ['<'] = '&lt;',
57
+ ['>'] = '&gt;',
58
+ ['&'] = '&amp;',
59
+ ['"'] = '&quot;',
60
+ ['\'']= '&#39;' }
61
+ return s:gsub("[<>&\"']", function(x) return map[x] end)
62
+ end
63
+
64
+ function unescape(s)
65
+ local map = { ['&lt;'] = '<',
66
+ ['&gt;'] = '>',
67
+ ['&amp;'] = '&',
68
+ ['&quot;'] = '"',
69
+ ['&#39;']= '\'' }
70
+ return s:gsub('(&(#?)([%d%a]+);)', function(x) return map[x] end)
71
+ end
72
+
73
+ -- Helper function to convert an attributes table into
74
+ -- a string that can be put into XML elements.
75
+ function attributes(attr)
76
+ local attr_table = {}
77
+ for x, y in pairsByKeys(attr) do
78
+ if y and y ~= '' then
79
+ table.insert(attr_table, string.format(' %s="%s"', x, escape(y)))
80
+ end
81
+ end
82
+ return table.concat(attr_table)
83
+ end
84
+
85
+ -- sort table, so that attributes are in consistent order
86
+ function pairsByKeys (t, f)
87
+ local a = {}
88
+ for n in pairs(t) do table.insert(a, n) end
89
+ table.sort(a, f)
90
+ local i = 0 -- iterator variable
91
+ local iter = function () -- iterator function
92
+ i = i + 1
93
+ if a[i] == nil then return nil
94
+ else return a[i], t[a[i]]
95
+ end
96
+ end
97
+ return iter
98
+ end
99
+
100
+ -- generic xml builder
101
+ function xml(tag, s, attr)
102
+ attr = attr and attributes(attr) or ''
103
+ s = s and '>' .. s .. '</' .. tag .. '>' or '/>'
104
+ return '<' .. tag .. attr .. s
105
+ end
106
+
107
+ -- Flatten nested table, needed for nested YAML metadata['
108
+ -- We only flatten associative arrays and create composite key,
109
+ -- numbered arrays and flat tables are left intact.
110
+ -- We also convert all hyphens in keys to underscores,
111
+ -- so that they are proper variable names
112
+ function flatten_table(tbl)
113
+ local result = {}
114
+
115
+ local function flatten(tbl, key)
116
+ for k, v in pairs(tbl) do
117
+ if type(k) == 'number' and k > 0 and k <= #tbl then
118
+ result[key] = tbl
119
+ break
120
+ else
121
+ k = (key and key .. '-' or '') .. k
122
+ if type(v) == 'table' then
123
+ flatten(v, k)
124
+ else
125
+ result[k] = v
126
+ end
127
+ end
128
+ end
129
+ end
130
+
131
+ flatten(tbl)
132
+ return result
133
+ end
134
+
135
+ -- Read a file from the working directory and
136
+ -- return its contents (or nil if not found).
137
+ function read_file(name)
138
+ local base, ext = name:match("([^%.]*)(.*)")
139
+ local fname = base .. ext
140
+ local file = io.open(fname, "r")
141
+ if not file then return nil end
142
+ return file:read("*all")
143
+ end
144
+
145
+ -- Parse YAML string and return table.
146
+ -- We only understand a subset.
147
+ function parse_yaml(s)
148
+ local l = {}
149
+ local c = {}
150
+ local i = 0
151
+ local k = nil
152
+
153
+ -- patterns
154
+ line_pattern = '(.-)\r?\n'
155
+ config_pattern = '^(%s*)([%w%-]+):%s*(.-)$'
156
+
157
+ -- First split string into lines
158
+ local function lines(line)
159
+ table.insert(l, line)
160
+ return ""
161
+ end
162
+
163
+ lines((s:gsub(line_pattern, lines)))
164
+
165
+ -- Then go over each line and check value and indentation
166
+ for _, v in ipairs(l) do
167
+ v:gsub(config_pattern, function(indent, tag, v)
168
+ if (v == '') then
169
+ i, k = string.len(indent), tag
170
+ c[tag] = {}
171
+ else
172
+ -- check whether value is enclosed by brackets, i.e. an array
173
+ if v:find('^%[(.-)%]$') then
174
+ arr = {};
175
+ for match in (v:sub(2, -2) .. ','):gmatch('(.-)' .. ',%s*') do
176
+ table.insert(arr, match);
177
+ end
178
+ v = arr;
179
+ else
180
+ -- if it is a string, remove optional enclosing quotes
181
+ v = v:match('^["\']*(.-)["\']*$')
182
+ end
183
+
184
+ if string.len(indent) == i + 2 and k then
185
+ c[k][tag] = v
186
+ else
187
+ c[tag] = v
188
+ end
189
+ end
190
+ end)
191
+ end
192
+
193
+ return c
194
+ end
195
+
196
+ -- add appropriate sec-type attribute
197
+ function sec_type_helper(s)
198
+ local map = { ['Abstract']= 'abstract',
199
+ ['Acknowledgments']= 'acknowledgements',
200
+ ['Author Summary']= 'author-summary',
201
+ ['Conclusions'] = 'conclusions',
202
+ ['Discussion'] = 'discussion',
203
+ ['Glossary'] = 'glossary',
204
+ ['Introduction'] = 'intro',
205
+ ['Materials and Methods'] = 'materials|methods',
206
+ ['Notes'] = 'notes',
207
+ ['References']= 'references',
208
+ ['Results']= 'results',
209
+ ['Supporting Information']= 'supplementary-material',
210
+ ['Supplementary Information']= 'supplementary-material' }
211
+ return map[s]
212
+ end
213
+
214
+ function section_helper(lev, s, title)
215
+ local attr = { ['sec-type'] = sec_type_helper(title) }
216
+
217
+ if attr['sec-type'] == "acknowledgements" then
218
+ table.insert(back, Ack(s, title))
219
+ elseif attr['sec-type'] == "references" then
220
+ table.insert(back, RefList(s, title))
221
+ elseif attr['sec-type'] == "notes" then
222
+ table.insert(back, Note(s, title))
223
+ elseif attr['sec-type'] == "glossary" then
224
+ table.insert(back, Glossary(s, title))
225
+ elseif attr['sec-type'] == "abstract" or attr['sec-type'] == "author-summary" then
226
+ -- discard, should be provided via metadata
227
+ elseif attr['sec-type'] == "supplementary-material" then
228
+ table.insert(sections, SupplementaryMaterial(s, title))
229
+ else
230
+ table.insert(sections, Section(lev, s, title, attr))
231
+ end
232
+
233
+ return attr
234
+ end
235
+
236
+ -- Create table with year, month, day and iso8601-formatted date
237
+ -- Input is iso8601-formatted date as string
238
+ -- Return nil if input is not a valid date
239
+ function date_helper(iso_date)
240
+ if not iso_date or string.len(iso_date) ~= 10 then return nil end
241
+
242
+ _,_,y,m,d = string.find(iso_date, '(%d+)-(%d+)-(%d+)')
243
+ time = os.time({ year = y, month = m, day = d })
244
+ date = os.date('*t', time)
245
+ date.iso8601 = string.format('%04d-%02d-%02d', date.year, date.month, date.day)
246
+ return date
247
+ end
248
+
249
+ -- Create affiliation table, linked to authors via aff-id
250
+ function affiliation_helper(tbl)
251
+
252
+ set = {}
253
+ i = 0
254
+ for _,author in ipairs(tbl.author) do
255
+ if author.affiliation then
256
+ if not set[author.affiliation] then
257
+ i = i + 1
258
+ set[author.affiliation] = i
259
+ end
260
+ author['aff-id'] = set[author.affiliation]
261
+ end
262
+ end
263
+
264
+ tbl.aff = {}
265
+ for k,v in pairs(set) do
266
+ aff = { id = v, name = k }
267
+ table.insert(tbl.aff, aff)
268
+ end
269
+
270
+ return tbl
271
+ end
272
+
273
+ -- Create corresponding author table, linked to authors via cor-id
274
+ function corresp_helper(tbl)
275
+
276
+ set = {}
277
+ i = 0
278
+ for _,author in ipairs(tbl.author) do
279
+ if author.corresp and author.email then
280
+ i = i + 1
281
+ set[i] = author.email
282
+ author['cor-id'] = i
283
+ end
284
+ end
285
+
286
+ tbl.corresp = {}
287
+ for k,v in pairs(set) do
288
+ corresp = { id = k, email = v }
289
+ table.insert(tbl.corresp, corresp)
290
+ end
291
+
292
+ return tbl
293
+ end
294
+
295
+ -- temporary fix
296
+ function fix_citeproc(s)
297
+ s = s:gsub('</surname>, ', '</surname>')
298
+ s = s:gsub('</name></name><name>','</name>')
299
+ return s
300
+ end
301
+
302
+ -- Convert pandoc alignment to something HTML can use.
303
+ -- align is AlignLeft, AlignRight, AlignCenter, or AlignDefault.
304
+ function html_align(align)
305
+ local map = { ['AlignRight']= 'right',
306
+ ['AlignCenter']= 'center' }
307
+ return map[align] or 'left'
308
+ end
309
+
310
+ -- Blocksep is used to separate block elements.
311
+ function Blocksep()
312
+ return "\n"
313
+ end
314
+
315
+ -- The functions that follow render corresponding pandoc elements.
316
+ -- s is always a string, attr is always a table of attributes, and
317
+ -- items is always an array of strings (the items in a list).
318
+ -- Comments indicate the types of other variables.
319
+ -- Defined at https://github.com/jgm/pandoc/blob/master/src/Text/Pandoc/Writers/Custom.hs
320
+
321
+ -- block elements
322
+
323
+ function Plain(s)
324
+ return s
325
+ end
326
+
327
+ function Para(s)
328
+ return xml('p', s)
329
+ end
330
+
331
+ function RawBlock(s)
332
+ return xml('preformat', s)
333
+ end
334
+
335
+ -- JATS restricts use to inside table cells (<td> and <th>)
336
+ function HorizontalRule()
337
+ return '<hr/>'
338
+ end
339
+
340
+ -- lev is an integer, the header level.
341
+ -- we can't use closing tags, as we don't know the end of the section
342
+ function Header(lev, s, attr)
343
+ attr = attr or {}
344
+ attr['lev'] = '' .. lev
345
+ return '</sec>\n<sec' .. attributes(attr) .. '>\n' .. xml('title', s)
346
+ end
347
+
348
+ function Note(s)
349
+ return s
350
+ end
351
+
352
+ function CodeBlock(s, attr)
353
+ -- If code block has class 'dot', pipe the contents through dot
354
+ -- and base64, and include the base64-encoded png as a data: URL.
355
+ if attr.class and string.match(' ' .. attr.class .. ' ',' dot ') then
356
+ local png = pipe("base64", pipe("dot -Tpng", s))
357
+ return '<img src="data:image/png;base64,' .. png .. '"/>'
358
+ -- otherwise treat as code (one could pipe through a highlighter)
359
+ else
360
+ return "<pre><code" .. attributes(attr) .. ">" .. escape(s) ..
361
+ "</code></pre>"
362
+ end
363
+ end
364
+
365
+ function BlockQuote(s)
366
+ xml('boxed-text', s)
367
+ end
368
+
369
+ -- Caption is a string, aligns is an array of strings,
370
+ -- widths is an array of floats, headers is an array of
371
+ -- strings, rows is an array of arrays of strings.
372
+ function Table(caption, aligns, widths, headers, rows)
373
+ local buffer = {}
374
+ local function add(s)
375
+ table.insert(buffer, s)
376
+ end
377
+ table.insert(buffer, '<table-wrap>')
378
+ if caption ~= '' then
379
+ -- if caption begins with <bold> text, make it the <title>
380
+ caption = string.gsub('<p>' .. caption, "^<p><bold>(.-)</bold>%s", "<title>%1</title>\n<p>")
381
+ add(xml('caption>', caption))
382
+ end
383
+ add("<table>")
384
+ if widths and widths[1] ~= 0 then
385
+ for _, w in pairs(widths) do
386
+ add('<col width="' .. string.format("%d%%", w * 100) .. '" />')
387
+ end
388
+ end
389
+ local header_row = {}
390
+ local empty_header = true
391
+ for i, h in pairs(headers) do
392
+ local align = html_align(aligns[i])
393
+
394
+ -- remove <p> tag
395
+ h = h:gsub("^<p>(.-)</p>", "%1")
396
+
397
+ table.insert(header_row,'<th align="' .. align .. '">' .. h .. '</th>')
398
+ empty_header = empty_header and h == ""
399
+ end
400
+ if empty_header then
401
+ head = ""
402
+ else
403
+ add('<tr>')
404
+ for _,h in pairs(header_row) do
405
+ add(h)
406
+ end
407
+ add('</tr>')
408
+ end
409
+ for _, row in pairs(rows) do
410
+ add('<tr>')
411
+ for i,c in pairs(row) do
412
+ -- remove <p> tag
413
+ c = c:gsub("^<p>(.-)</p>", "%1")
414
+ add('<td align="' .. html_align(aligns[i]) .. '">' .. c .. '</td>')
415
+ end
416
+ add('</tr>')
417
+ end
418
+ add('</table>\n</table-wrap>')
419
+ return table.concat(buffer,'\n')
420
+ end
421
+
422
+ function BulletList(items)
423
+ local attr = { ['list-type'] = 'bullet' }
424
+ return List(items, attr)
425
+ end
426
+
427
+ function OrderedList(items)
428
+ local attr = { ['list-type'] = 'order' }
429
+ return List(items, attr)
430
+ end
431
+
432
+ function List(items, attr)
433
+ local buffer = {}
434
+ for _, item in pairs(items) do
435
+ table.insert(buffer, xml('list-item', item))
436
+ end
437
+ return xml('list', '\n' .. table.concat(buffer, '\n') .. '\n', attr)
438
+ end
439
+
440
+ -- Revisit association list StackValue instance.
441
+ -- items is a table of tables
442
+ function DefinitionList(items)
443
+ local buffer = {}
444
+ for _,item in pairs(items) do
445
+ for k, v in pairs(item) do
446
+ local term = xml('term', k)
447
+ local def = xml('def', table.concat(v,'</def><def>'))
448
+ table.insert(buffer, xml('def-item', term .. def))
449
+ end
450
+ end
451
+ return xml('def-list', '\n' .. table.concat(buffer, '\n') .. '\n')
452
+ end
453
+
454
+ function Div(s, attr)
455
+ return s
456
+ end
457
+
458
+ -- custom block elements for JATS
459
+
460
+ -- section is generated after header to allow reordering
461
+ function Section(lev, s, title, attr)
462
+ local last = headers[#headers]
463
+ local h = last and last.h or {}
464
+ h[lev] = (h[lev] or 0) + 1
465
+ for i = lev + 1, #headers do
466
+ table.remove(h, i)
467
+ end
468
+
469
+ local header = { ['h'] = h,
470
+ ['title'] = title,
471
+ ['id'] = 'sec-' .. table.concat(h,'.'),
472
+ ['sec-type'] = attr['sec-type'] }
473
+
474
+ table.insert(headers, header)
475
+
476
+ attr = { ['id'] = header['id'], ['sec-type'] = header['sec-type'] }
477
+ title = xml('title', title ~= '' and title or nil)
478
+ return xml('sec', '\n' .. title .. s, attr)
479
+ end
480
+
481
+ function SupplementaryMaterial(s, title, attr)
482
+ attr = {}
483
+ title = xml('title', title)
484
+ local caption = xml('caption', title .. s)
485
+ return xml('supplementary-material', '\n' .. caption .. '\n', attr)
486
+ end
487
+
488
+ function Ack(s, title)
489
+ title = title and '\n' .. xml('title', title) or ''
490
+ return xml('ack', title .. s)
491
+ end
492
+
493
+ function Glossary(s, title, attr)
494
+ title = xml('title', title)
495
+ return xml('glossary', title .. s, attr)
496
+ end
497
+
498
+ function RefList(s, title)
499
+ s = fix_citeproc(s)
500
+
501
+ -- format ids
502
+ s = string.gsub(s, '<ref id="(%d+)">', function (r)
503
+ local attr = { ['id'] = string.format('r%03d', tonumber(r)) }
504
+ return '<ref ' .. attributes(attr) .. '>'
505
+ end)
506
+
507
+ for ref in string.gmatch(s, '(<ref.-</ref>)') do
508
+ Ref(ref)
509
+ end
510
+
511
+ if #references > 0 then
512
+ title = xml('title', title)
513
+ return xml('ref-list', title .. table.concat(references, '\n'), attr)
514
+ else
515
+ return ''
516
+ end
517
+ end
518
+
519
+ function Ref(s)
520
+ table.insert(references, s)
521
+ return #references
522
+ end
523
+
524
+ -- inline elements
525
+
526
+ function Str(s)
527
+ return s
528
+ end
529
+
530
+ function Space()
531
+ return ' '
532
+ end
533
+
534
+ function Emph(s)
535
+ return xml('italic', s)
536
+ end
537
+
538
+ function Strong(s)
539
+ return xml('bold', s)
540
+ end
541
+
542
+ function Strikeout(s)
543
+ return xml('strike', s)
544
+ end
545
+
546
+ function Superscript(s)
547
+ return xml('sup', s)
548
+ end
549
+
550
+ function Subscript(s)
551
+ return xml('sub', s)
552
+ end
553
+
554
+ function SmallCaps(s)
555
+ return xml('sc', s)
556
+ end
557
+
558
+ function SingleQuoted(s)
559
+ return "'" .. s .. "'"
560
+ end
561
+
562
+ function DoubleQuoted(s)
563
+ return '"' .. s .. '"'
564
+ end
565
+
566
+ -- format in-text citation
567
+ function Cite(s)
568
+ local ids = {}
569
+ for id in string.gmatch(s, '(%d+)') do
570
+ id = tonumber(id)
571
+ -- workaround to discard year mistakenly taken for key
572
+ if id and id < 1000 then
573
+ local attr = { ['ref-type'] = 'bibr',
574
+ ['rid'] = string.format("r%03d", id) }
575
+ table.insert(ids, xml('xref', '[' .. id .. ']', attr))
576
+ end
577
+ end
578
+ if #ids > 0 then
579
+ return table.concat(ids)
580
+ else
581
+ -- return original key for backwards compatibility
582
+ return s
583
+ end
584
+ end
585
+
586
+ function Code(s, attr)
587
+ return xml('preformat', s, attr)
588
+ end
589
+
590
+ function DisplayMath(s)
591
+ return xml('disp-formula', s)
592
+ end
593
+
594
+ function InlineMath(s)
595
+ return xml('inline-formula', s)
596
+ end
597
+
598
+ function RawInline(s)
599
+ return xml('preformat', s)
600
+ end
601
+
602
+ function LineBreak()
603
+ return ' '
604
+ end
605
+
606
+ function Link(s, src, title)
607
+ if src ~= '' and s ~= '' then
608
+ attr = { ['ext-link-type'] = 'uri',
609
+ ['xlink:href'] = escape(src),
610
+ ['xlink:title'] = escape(title),
611
+ ['xlink:type'] = 'simple' }
612
+
613
+ return xml('ext-link', s, attr)
614
+ else
615
+ return s
616
+ end
617
+ end
618
+
619
+ function CaptionedImage(s, src, title)
620
+ -- if title begins with <bold> text, make it the <title>
621
+ title = string.gsub(title, "^<bold>(.-)</bold>%s", function(t) xml('title', t) end)
622
+ local num = #figures + 1
623
+ local attr = { ['id'] = string.format("g%03d", num) }
624
+ local caption = xml('caption', s)
625
+ local fig = xml('fig', caption .. Image(nil, src, title), attr)
626
+
627
+ table.insert(figures, fig)
628
+ return fig
629
+ end
630
+
631
+ function Image(s, src, title)
632
+ local attr = { ['mimetype'] = 'image',
633
+ ['xlink:href'] = escape(src),
634
+ ['xlink:title'] = escape(title),
635
+ ['xlink:type'] = 'simple' }
636
+
637
+ return xml('graphic', s, attr)
638
+ end
639
+
640
+ -- handle bold and italic
641
+ function Span(s, attr)
642
+ if attr.style == "font-weight:bold" then
643
+ return Strong(s)
644
+ elseif attr.style == "font-style:italic" then
645
+ return Emph(s)
646
+ elseif attr.style == "font-variant: small-caps" then
647
+ return SmallCaps(s)
648
+ else
649
+ return s
650
+ end
651
+ end
652
+
653
+ -- The following code will produce runtime warnings when you haven't defined
654
+ -- all of the functions you need for the custom writer, so it's useful
655
+ -- to include when you're working on a writer.
656
+ local meta = {}
657
+ meta.__index =
658
+ function(_, key)
659
+ io.stderr:write(string.format("WARNING: Undefined function '%s'\n",key))
660
+ return function() return "" end
661
+ end
662
+ setmetatable(_G, meta)