@fiduswriter/document 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +16 -0
  3. package/jest.config.js +23 -0
  4. package/package.json +59 -0
  5. package/schema.json +1 -0
  6. package/scripts/export-schema.js +16 -0
  7. package/src/bibliography/common.js +92 -0
  8. package/src/bibliography/csl_bib.js +139 -0
  9. package/src/citations/citeproc_sys.js +42 -0
  10. package/src/citations/format.js +194 -0
  11. package/src/common/blob.js +10 -0
  12. package/src/common/file.js +25 -0
  13. package/src/common/index.js +12 -0
  14. package/src/common/network.js +79 -0
  15. package/src/common/text.js +44 -0
  16. package/src/editor/e2ee/encryptor.js +228 -0
  17. package/src/exporter/docx/citations.js +177 -0
  18. package/src/exporter/docx/comments.js +165 -0
  19. package/src/exporter/docx/footnotes.js +240 -0
  20. package/src/exporter/docx/images.js +101 -0
  21. package/src/exporter/docx/index.js +185 -0
  22. package/src/exporter/docx/lists.js +260 -0
  23. package/src/exporter/docx/math.js +46 -0
  24. package/src/exporter/docx/metadata.js +289 -0
  25. package/src/exporter/docx/rels.js +193 -0
  26. package/src/exporter/docx/render.js +941 -0
  27. package/src/exporter/docx/richtext.js +1182 -0
  28. package/src/exporter/docx/tables.js +112 -0
  29. package/src/exporter/docx/tools.js +50 -0
  30. package/src/exporter/epub/index.js +142 -0
  31. package/src/exporter/epub/templates.js +140 -0
  32. package/src/exporter/epub/tools.js +96 -0
  33. package/src/exporter/html/citations.js +121 -0
  34. package/src/exporter/html/convert.js +813 -0
  35. package/src/exporter/html/index.js +192 -0
  36. package/src/exporter/html/templates.js +34 -0
  37. package/src/exporter/html/tools.js +50 -0
  38. package/src/exporter/jats/bibliography.js +183 -0
  39. package/src/exporter/jats/citations.js +109 -0
  40. package/src/exporter/jats/convert.js +871 -0
  41. package/src/exporter/jats/index.js +92 -0
  42. package/src/exporter/jats/templates.js +35 -0
  43. package/src/exporter/jats/text.js +72 -0
  44. package/src/exporter/latex/convert.js +934 -0
  45. package/src/exporter/latex/escape_latex.js +21 -0
  46. package/src/exporter/latex/index.js +74 -0
  47. package/src/exporter/latex/readme.js +22 -0
  48. package/src/exporter/native/shrink.js +132 -0
  49. package/src/exporter/odt/citations.js +101 -0
  50. package/src/exporter/odt/footnotes.js +147 -0
  51. package/src/exporter/odt/images.js +115 -0
  52. package/src/exporter/odt/index.js +156 -0
  53. package/src/exporter/odt/math.js +57 -0
  54. package/src/exporter/odt/metadata.js +251 -0
  55. package/src/exporter/odt/render.js +806 -0
  56. package/src/exporter/odt/richtext.js +865 -0
  57. package/src/exporter/odt/styles.js +387 -0
  58. package/src/exporter/odt/track.js +68 -0
  59. package/src/exporter/pandoc/citations.js +98 -0
  60. package/src/exporter/pandoc/convert.js +1017 -0
  61. package/src/exporter/pandoc/index.js +92 -0
  62. package/src/exporter/pandoc/readme.js +8 -0
  63. package/src/exporter/pandoc/tools.js +51 -0
  64. package/src/exporter/print/index.js +177 -0
  65. package/src/exporter/tools/doc_content.js +144 -0
  66. package/src/exporter/tools/file.js +9 -0
  67. package/src/exporter/tools/json.js +73 -0
  68. package/src/exporter/tools/svg.js +29 -0
  69. package/src/exporter/tools/xml.js +531 -0
  70. package/src/exporter/tools/xml_zip.js +95 -0
  71. package/src/exporter/tools/zip.js +90 -0
  72. package/src/exporter/tools/zotero_csl.js +93 -0
  73. package/src/importer/citations.js +129 -0
  74. package/src/importer/docx/citations.js +123 -0
  75. package/src/importer/docx/convert.js +1427 -0
  76. package/src/importer/docx/helpers.js +9 -0
  77. package/src/importer/docx/omml2mathml.js +1448 -0
  78. package/src/importer/docx/parse.js +735 -0
  79. package/src/importer/native/get_images.js +76 -0
  80. package/src/importer/native/update.js +29 -0
  81. package/src/importer/odt/citations.js +87 -0
  82. package/src/importer/odt/convert.js +1855 -0
  83. package/src/importer/pandoc/convert.js +884 -0
  84. package/src/importer/pandoc/helpers.js +84 -0
  85. package/src/importer/zip_analyzer.js +102 -0
  86. package/src/index.js +1 -0
  87. package/src/mathlive/opf_includes.js +24 -0
  88. package/src/schema/common/annotate.js +76 -0
  89. package/src/schema/common/base.js +118 -0
  90. package/src/schema/common/citation.js +62 -0
  91. package/src/schema/common/equation.js +31 -0
  92. package/src/schema/common/figure.js +190 -0
  93. package/src/schema/common/heading.js +43 -0
  94. package/src/schema/common/index.js +40 -0
  95. package/src/schema/common/list.js +95 -0
  96. package/src/schema/common/reference.js +100 -0
  97. package/src/schema/common/table.js +103 -0
  98. package/src/schema/common/track.js +190 -0
  99. package/src/schema/const.js +58 -0
  100. package/src/schema/convert.js +1272 -0
  101. package/src/schema/document/content.js +187 -0
  102. package/src/schema/document/index.js +117 -0
  103. package/src/schema/document/structure.js +452 -0
  104. package/src/schema/export.js +21 -0
  105. package/src/schema/footnotes.js +126 -0
  106. package/src/schema/footnotes_convert.js +31 -0
  107. package/src/schema/i18n.js +595 -0
  108. package/src/schema/index.js +5 -0
  109. package/src/schema/mini_json.js +61 -0
  110. package/src/schema/text.js +22 -0
@@ -0,0 +1,934 @@
1
+ import {BIBLIOGRAPHY_HEADERS, CATS} from "../../schema/i18n.js"
2
+ import {escapeLatexText} from "./escape_latex.js"
3
+
4
+ export class LatexExporterConvert {
5
+ constructor(exporter, imageDB, bibDB, settings) {
6
+ this.exporter = exporter
7
+ this.settings = settings
8
+ this.imageDB = imageDB
9
+ this.bibDB = bibDB
10
+ this.imageIds = []
11
+ this.usedBibDB = {}
12
+ // While walking the tree, we take note of the kinds of features That
13
+ // are present in the file, so that we can assemble an preamble and
14
+ // epilogue based on our findings.
15
+ this.features = {}
16
+ this.internalLinks = []
17
+ this.categoryCounter = {} // counters for each type of figure (figure/table/photo)
18
+ this.authorsTex = ""
19
+ }
20
+
21
+ init(docContent) {
22
+ this.preWalkJson(docContent)
23
+ const rawTransformation = this.walkJson(docContent)
24
+ const body = this.postProcess(rawTransformation)
25
+ const copyright = this.assembleCopyright()
26
+ const preamble = this.assemblePreamble()
27
+ const epilogue = this.assembleEpilogue()
28
+ const latex =
29
+ copyright +
30
+ this.docDeclaration +
31
+ preamble +
32
+ this.authorsTex +
33
+ "\n\\begin{document}\n" +
34
+ body +
35
+ epilogue +
36
+ "\n\\end{document}\n"
37
+ const returnObject = {
38
+ latex,
39
+ imageIds: this.imageIds,
40
+ usedBibDB: this.usedBibDB
41
+ }
42
+ return returnObject
43
+ }
44
+
45
+ get docDeclaration() {
46
+ return "\\documentclass{article}\n"
47
+ }
48
+
49
+ // Check for things needed before creating raw transform
50
+ preWalkJson(node) {
51
+ switch (node.type) {
52
+ // Collect all internal links so that we only set the anchors for those
53
+ // that are being linked to.
54
+ case "text":
55
+ if (node.marks) {
56
+ const hyperlink = node.marks.find(
57
+ mark => mark.type === "link"
58
+ )
59
+ if (hyperlink) {
60
+ const href = hyperlink.attrs.href
61
+ if (
62
+ href[0] === "#" &&
63
+ !this.internalLinks.includes(href)
64
+ ) {
65
+ this.internalLinks.push(href.slice(1))
66
+ }
67
+ }
68
+ }
69
+ break
70
+ }
71
+ if (node.content) {
72
+ node.content.forEach(child => this.preWalkJson(child))
73
+ }
74
+ }
75
+
76
+ walkJson(node, options = {}) {
77
+ let start = "",
78
+ content = "",
79
+ end = "",
80
+ placeFootnotesAfterBlock = false
81
+ switch (node.type) {
82
+ case "doc":
83
+ break
84
+ case "title":
85
+ start += "\n\\title{"
86
+ end = "}" + end
87
+ break
88
+ case "heading_part":
89
+ if (node.attrs.metadata === "subtitle" && node.content) {
90
+ start += "\n\\subtitle{"
91
+ end = "}" + end
92
+ this.features.subtitle = true
93
+ options = Object.assign({}, options)
94
+ options.ignoreHeading = true
95
+ } else if (!options.madeTitle) {
96
+ start += "\n\n\\maketitle\n"
97
+ options.madeTitle = true
98
+ }
99
+ break
100
+ case "contributor":
101
+ // Ignore - we deal with contributors_part instead.
102
+ break
103
+ case "contributors_part":
104
+ if (node.content) {
105
+ const contributorLabels = {
106
+ authors: gettext("Authors"),
107
+ editors: gettext("Editors"),
108
+ translators: gettext("Translators"),
109
+ reviewers: gettext("Reviewers"),
110
+ contributors: gettext("Contributors")
111
+ }
112
+ const roleLabel = contributorLabels[node.attrs.metadata]
113
+
114
+ if (node.attrs.metadata === "authors") {
115
+ const authorsPerAffil = node.content
116
+ .map(node => {
117
+ const author = node.attrs,
118
+ nameParts = []
119
+ let affiliation = false
120
+ if (author.firstname) {
121
+ nameParts.push(author.firstname)
122
+ }
123
+ if (author.lastname) {
124
+ nameParts.push(author.lastname)
125
+ }
126
+ if (nameParts.length && author.institution) {
127
+ affiliation = author.institution
128
+ } else if (author.institution) {
129
+ // We have an institution but no names. Use institution as name.
130
+ nameParts.push(author.institution)
131
+ }
132
+ return {
133
+ name: nameParts.join(" "),
134
+ affiliation,
135
+ email: author.email,
136
+ id_type: author.id_type,
137
+ id_value: author.id_value
138
+ }
139
+ })
140
+ .reduce((affils, author) => {
141
+ const affil = author.affiliation
142
+ affils[affil] = affils[affil] || []
143
+ affils[affil].push(author)
144
+ return affils
145
+ }, {})
146
+
147
+ Object.values(authorsPerAffil).forEach(affil => {
148
+ affil.forEach(author => {
149
+ let thanks = ""
150
+ if (author.email) {
151
+ thanks += `\\thanks{${escapeLatexText(author.email)}}`
152
+ }
153
+ if (author.id_type && author.id_value) {
154
+ thanks += `\\thanks{${escapeLatexText(author.id_type)}: ${escapeLatexText(author.id_value)}}`
155
+ }
156
+ this.authorsTex += `\n\\author{${escapeLatexText(author.name)}${thanks}}`
157
+ })
158
+
159
+ this.authorsTex += `\n\\affil{${
160
+ affil[0].affiliation
161
+ ? escapeLatexText(affil[0].affiliation)
162
+ : ""
163
+ }}`
164
+ })
165
+ this.authorsTex += "\n\n"
166
+ this.features.authors = true
167
+ } else {
168
+ if (!options.madeTitle) {
169
+ start += "\n\n\\maketitle\n"
170
+ options.madeTitle = true
171
+ }
172
+ const contributorNames = node.content
173
+ .map(contributorNode => {
174
+ const attrs = contributorNode.attrs
175
+ const nameParts = []
176
+ if (attrs.firstname) {
177
+ nameParts.push(attrs.firstname)
178
+ }
179
+ if (attrs.lastname) {
180
+ nameParts.push(attrs.lastname)
181
+ }
182
+ if (!nameParts.length && attrs.institution) {
183
+ // We have an institution but no names. Use institution as name.
184
+ nameParts.push(attrs.institution)
185
+ }
186
+ let name = nameParts.join(" ")
187
+ if (attrs.id_type && attrs.id_value) {
188
+ name += ` (${escapeLatexText(attrs.id_type)}: ${escapeLatexText(attrs.id_value)})`
189
+ }
190
+ return name
191
+ })
192
+ .filter(name => name.length)
193
+ .join(", ")
194
+ if (contributorNames.length) {
195
+ content += `\n\\noindent\\textbf{${roleLabel}:} ${contributorNames}\n\n`
196
+ }
197
+ }
198
+ }
199
+
200
+ break
201
+ case "tags_part":
202
+ if (node.content) {
203
+ if (node.attrs.metadata === "keywords") {
204
+ start += "\n\\keywords{"
205
+ end = "}" + end
206
+ this.features.keywords = true
207
+ } else if (!options.madeTitle) {
208
+ start += "\n\n\\maketitle\n"
209
+ options.madeTitle = true
210
+ }
211
+ content += node.content
212
+ .map(keyword => escapeLatexText(keyword.attrs.tag))
213
+ .join("\\sep ")
214
+ }
215
+ break
216
+ case "tag":
217
+ // Ignore - we already took all the tags_part from the keywords node.
218
+ break
219
+ case "richtext_part":
220
+ if (!options.madeTitle) {
221
+ start += "\n\n\\maketitle\n"
222
+ options.madeTitle = true
223
+ }
224
+ if (node.content && node.attrs.metadata === "abstract") {
225
+ start += "\n\\begin{abstract}\n"
226
+ end = "\n\\end{abstract}\n" + end
227
+ }
228
+ break
229
+ case "table_of_contents":
230
+ start += "\n\n\\tableofcontents\n"
231
+ break
232
+ case "separator_part":
233
+ case "table_part":
234
+ // part separators as in page breaks should usually already be handled
235
+ // by LaTeX and table parts will simply show the table inside of them.
236
+ break
237
+ case "paragraph":
238
+ start += "\n\n"
239
+ end = "\n" + end
240
+ break
241
+ case "heading1":
242
+ case "heading2":
243
+ case "heading3":
244
+ case "heading4":
245
+ case "heading5":
246
+ case "heading6": {
247
+ if (options.ignoreHeading) {
248
+ break
249
+ }
250
+ const level = Number.parseInt(node.type.slice(-1))
251
+ switch (level) {
252
+ case 1:
253
+ start += "\n\n\\section{"
254
+ break
255
+ case 2:
256
+ start += "\n\n\\subsection{"
257
+ break
258
+ case 3:
259
+ case 4:
260
+ case 5:
261
+ case 6:
262
+ // TODO: Add support for levels 4/5/6
263
+ start += "\n\n\\subsubsection{"
264
+ break
265
+ }
266
+ end = `}\\label{${node.attrs.id}}\n\n` + end
267
+ // Check if this heading is being linked to. If this is the case,
268
+ // place a protected hypertarget here that does not add an extra
269
+ // entry into the PDF TOC.
270
+ if (this.internalLinks.includes(node.attrs.id)) {
271
+ // Add a link target
272
+ end =
273
+ end +
274
+ `\\texorpdfstring{\\protect\\hypertarget{${node.attrs.id}}{}}{}`
275
+ }
276
+ options = Object.assign({}, options)
277
+ options.noLineBreak = true
278
+ if (!options.onlyFootnoteMarkers) {
279
+ placeFootnotesAfterBlock = true
280
+ options.onlyFootnoteMarkers = true
281
+ options.unplacedFootnotes = []
282
+ }
283
+ break
284
+ }
285
+ case "code_block": {
286
+ // Support language and category attributes
287
+ if (node.attrs.category && node.attrs.id) {
288
+ const language = this.doc.attrs.language || "en-US"
289
+ const {CATS} = require("../../schema/i18n")
290
+ const categoryLabel =
291
+ CATS[node.attrs.category]?.[language] ||
292
+ node.attrs.category
293
+
294
+ // Count code blocks to get the number
295
+ const categories = {}
296
+ this.doc.descendants(n => {
297
+ if (
298
+ n.type === "code_block" &&
299
+ n.attrs.category &&
300
+ n.attrs.id
301
+ ) {
302
+ if (!categories[n.attrs.category]) {
303
+ categories[n.attrs.category] = 0
304
+ }
305
+ categories[n.attrs.category]++
306
+ if (n.attrs.id === node.attrs.id) {
307
+ return false
308
+ }
309
+ }
310
+ })
311
+ const number = categories[node.attrs.category] || 1
312
+ const caption = node.attrs.title
313
+ ? `${categoryLabel} ${number}: ${this.convertText(node.attrs.title)}`
314
+ : `${categoryLabel} ${number}`
315
+
316
+ start += `\n\\begin{listing}\n\\caption{${caption}}\\label{${node.attrs.id}}\n\\begin{code}\n\n`
317
+ end = `\n\n\\end{code}\n\\end{listing}\n` + end
318
+ this.features.listing = true
319
+ } else if (node.attrs.language) {
320
+ start += `\n\\begin{code}[${this.convertText(node.attrs.language)}]\n\n`
321
+ end = `\n\n\\end{code}\n` + end
322
+ } else {
323
+ start += "\n\\begin{code}\n\n"
324
+ end = "\n\n\\end{code}\n" + end
325
+ }
326
+ this.features.code = true
327
+ break
328
+ }
329
+ case "blockquote":
330
+ start += "\n\\begin{quote}\n\n"
331
+ end = "\n\n\\end{quote}\n" + end
332
+ break
333
+ case "ordered_list":
334
+ if (node.attrs.order !== 1) {
335
+ start += `\n\\begin{enumerate}[start=${node.attrs.order}]`
336
+ this.features.orderedListStart = true
337
+ } else {
338
+ start += "\n\\begin{enumerate}"
339
+ }
340
+ end = "\n\\end{enumerate}" + end
341
+ if (!options.onlyFootnoteMarkers) {
342
+ placeFootnotesAfterBlock = true
343
+ options = Object.assign({}, options)
344
+ options.onlyFootnoteMarkers = true
345
+ options.unplacedFootnotes = []
346
+ }
347
+ break
348
+ case "bullet_list":
349
+ start += "\n\\begin{itemize}"
350
+ end = "\n\\end{itemize}" + end
351
+ if (!options.onlyFootnoteMarkers) {
352
+ placeFootnotesAfterBlock = true
353
+ options = Object.assign({}, options)
354
+ options.onlyFootnoteMarkers = true
355
+ options.unplacedFootnotes = []
356
+ }
357
+ break
358
+ case "list_item":
359
+ start += "\n\\item "
360
+ end = "\n" + end
361
+ break
362
+ case "footnote":
363
+ if (options.onlyFootnoteMarkers) {
364
+ // We are inside a headline or a list and can only place a
365
+ // footnote marker here. The footnote will have to be put
366
+ // beyond the block node instead.
367
+ start += "\\protect\\footnotemark{}"
368
+ options.unplacedFootnotes.push(node.attrs.footnote)
369
+ } else {
370
+ if (
371
+ !node.attrs.footnote.find(par => par.type === "figure")
372
+ ) {
373
+ // LaTeX doesn't allow figures in footnotes, so well move
374
+ // this footnote into the regular text.
375
+ start += "\\footnote{"
376
+ end = "}" + end
377
+ }
378
+ let fnContent = ""
379
+ node.attrs.footnote.forEach(footPar => {
380
+ fnContent += this.walkJson(footPar, options)
381
+ })
382
+ content += fnContent.replace(/^\s+|\s+$/g, "")
383
+ }
384
+ break
385
+ case "text": {
386
+ let strong, em, underline, hyperlink, anchor, sup, sub, code
387
+ // Check for hyperlink, bold/strong, italic/em and underline
388
+ if (node.marks) {
389
+ strong = node.marks.find(mark => mark.type === "strong")
390
+ em = node.marks.find(mark => mark.type === "em")
391
+ underline = node.marks.find(
392
+ mark => mark.type === "underline"
393
+ )
394
+ hyperlink = node.marks.find(mark => mark.type === "link")
395
+ anchor = node.marks.find(mark => mark.type === "anchor")
396
+ sup = node.marks.find(mark => mark.type === "sup")
397
+ sub = node.marks.find(mark => mark.type === "sub")
398
+ code = node.marks.find(mark => mark.type === "code")
399
+ }
400
+ if (em) {
401
+ start += "\\emph{"
402
+ end = "}" + end
403
+ }
404
+ if (strong) {
405
+ start += "\\textbf{"
406
+ end = "}" + end
407
+ }
408
+ if (underline) {
409
+ start += "\\underline{"
410
+ end = "}" + end
411
+ }
412
+ if (sup) {
413
+ start += "\\textsuperscript{"
414
+ end = "}" + end
415
+ }
416
+ if (sub) {
417
+ start += "\\textsubscript{"
418
+ end = "}" + end
419
+ }
420
+ if (code) {
421
+ start += "\\texttt{"
422
+ end = "}" + end
423
+ }
424
+ if (hyperlink) {
425
+ const href = hyperlink.attrs.href
426
+ if (href[0] === "#") {
427
+ // Internal link
428
+ start += `\\hyperlink{${href.slice(1)}}{`
429
+ } else {
430
+ // External link
431
+ start += `\\href{${href}}{`
432
+ }
433
+ end = "}" + end
434
+ this.features.hyperlinks = true
435
+ }
436
+ if (anchor && this.internalLinks.includes(anchor.attrs.id)) {
437
+ // Add a link target
438
+ start += `\\hypertarget{${anchor.attrs.id}}{`
439
+ end = "}" + end
440
+ }
441
+ content += escapeLatexText(node.text)
442
+ break
443
+ }
444
+ case "cross_reference": {
445
+ content += `\\hyperref[${node.attrs.id}]{${node.attrs.title || "MISSING TARGET"}}`
446
+ this.features.hyperlinks = true
447
+ break
448
+ }
449
+ case "citation": {
450
+ const references = node.attrs.references
451
+ const format = node.attrs.format
452
+ let citationCommand = "\\" + format
453
+
454
+ if (
455
+ references.length > 1 &&
456
+ references.every(ref => !ref.locator && !ref.prefix)
457
+ ) {
458
+ // multi source citation without page numbers or text before.
459
+ const citationEntryKeys = []
460
+
461
+ const allCitationItemsPresent = references
462
+ .map(ref => ref.id)
463
+ .every(citationEntry => {
464
+ const bibDBEntry = this.bibDB.db[citationEntry]
465
+ if (bibDBEntry) {
466
+ if (!bibDBEntry) {
467
+ // Not present in bibliography database, skip it.
468
+ // TODO: Throw an error?
469
+ return false
470
+ }
471
+ if (!this.usedBibDB[citationEntry]) {
472
+ const citationKey =
473
+ this.createUniqueCitationKey(
474
+ bibDBEntry.entry_key
475
+ )
476
+ this.usedBibDB[citationEntry] =
477
+ Object.assign({}, bibDBEntry)
478
+ this.usedBibDB[citationEntry].entry_key =
479
+ citationKey
480
+ }
481
+ citationEntryKeys.push(
482
+ this.usedBibDB[citationEntry].entry_key
483
+ )
484
+ }
485
+ return true
486
+ })
487
+ if (allCitationItemsPresent) {
488
+ citationCommand += `{${citationEntryKeys.join(",")}}`
489
+ } else {
490
+ citationCommand = false
491
+ }
492
+ } else {
493
+ if (references.length > 1) {
494
+ citationCommand += "s" // Switching from \autocite to \autocites
495
+ }
496
+
497
+ const allCitationItemsPresent = references.every(ref => {
498
+ const bibDBEntry = this.bibDB.db[ref.id]
499
+ if (!bibDBEntry) {
500
+ // Not present in bibliography database, skip it.
501
+ // TODO: Throw an error?
502
+ return false
503
+ }
504
+
505
+ if (ref.prefix) {
506
+ citationCommand += `[${ref.prefix}]`
507
+ if (!ref.locator) {
508
+ citationCommand += "[]"
509
+ }
510
+ }
511
+ if (ref.locator) {
512
+ citationCommand += `[${ref.locator}]`
513
+ }
514
+ citationCommand += "{"
515
+
516
+ if (!this.usedBibDB[ref.id]) {
517
+ const citationKey = this.createUniqueCitationKey(
518
+ bibDBEntry.entry_key
519
+ )
520
+ this.usedBibDB[ref.id] = Object.assign(
521
+ {},
522
+ bibDBEntry
523
+ )
524
+ this.usedBibDB[ref.id].entry_key = citationKey
525
+ }
526
+ citationCommand += this.usedBibDB[ref.id].entry_key
527
+ citationCommand += "}"
528
+
529
+ return true
530
+ })
531
+
532
+ if (!allCitationItemsPresent) {
533
+ citationCommand = false
534
+ }
535
+ }
536
+ if (citationCommand) {
537
+ content += citationCommand
538
+ this.features.citations = true
539
+ }
540
+ break
541
+ }
542
+ case "figure": {
543
+ const category = node.attrs.category
544
+ const captionContent = node.attrs.caption
545
+ ? node.content.find(node => node.type === "figure_caption")
546
+ ?.content || []
547
+ : []
548
+ let caption
549
+ if (category !== "none") {
550
+ if (!this.categoryCounter[category]) {
551
+ this.categoryCounter[category] = 1
552
+ }
553
+ const catCount = this.categoryCounter[category]++
554
+ const catLabel = `${CATS[category][this.settings.language]} ${catCount}`
555
+ if (captionContent.length) {
556
+ caption = `${catLabel}: ${captionContent.map(node => this.walkJson(node)).join("")}`
557
+ } else {
558
+ caption = catLabel
559
+ }
560
+ } else {
561
+ caption = captionContent
562
+ .map(node => this.walkJson(node))
563
+ .join("")
564
+ }
565
+ let innerFigure = ""
566
+ let copyright
567
+ const image =
568
+ node.content.find(node => node.type === "image")?.attrs
569
+ .image || false
570
+ if (image) {
571
+ this.imageIds.push(image)
572
+ const imageDBEntry = this.imageDB.db[image],
573
+ filePathName = imageDBEntry.image,
574
+ filename = filePathName.split("/").pop()
575
+ copyright = imageDBEntry.copyright
576
+ if (filename.split(".").pop() === "svg") {
577
+ innerFigure += `\\includesvg[width=${Number.parseInt(node.attrs.width) / 100}\\textwidth]{${filename}}\n`
578
+ this.features.SVGs = true
579
+ } else {
580
+ innerFigure += `\\scaledgraphics{${filename}}{${Number.parseInt(node.attrs.width) / 100}}\n`
581
+ this.features.images = true
582
+ }
583
+ } else {
584
+ const equation =
585
+ node.content.find(
586
+ node => node.type === "figure_equation"
587
+ )?.attrs.equation || ""
588
+ innerFigure += `\\begin{displaymath}\n${equation}\n\\end{displaymath}\n`
589
+ }
590
+ if (category === "table") {
591
+ const aligned =
592
+ node.attrs.width === "100" ? "left" : node.attrs.aligned
593
+ if (aligned === "center") {
594
+ start += "\n\n\\begin{center}"
595
+ end = "\n\n\\end{center}\n" + end
596
+ } else if (aligned === "right") {
597
+ start += "\n\n{\\raggedleft" // This is not a typo - raggedleft = aligned: right
598
+ end = "\n\n}\n" + end
599
+ } // aligned === 'left' is default
600
+ start += "\n\\begin{table}\n"
601
+ content += caption.length ? `\\caption*{${caption}}` : ""
602
+ content += `\\label{${node.attrs.id}}\n${innerFigure}`
603
+ end = "\\end{table}\n" + end
604
+ } else {
605
+ // TODO: handle photo figure types in a special way
606
+ if (
607
+ node.attrs.width === "100" ||
608
+ node.attrs.aligned === "center"
609
+ ) {
610
+ start += "\n\\begin{figure}\n"
611
+ end = "\\end{figure}\n" + end
612
+ } else {
613
+ const aligned = node.attrs.aligned[0]
614
+ start += `\n\\begin{wrapfigure}{${aligned}}{${Number.parseInt(node.attrs.width) / 100}\\textwidth}\n`
615
+ end = "\\end{wrapfigure}\n" + end
616
+ this.features.wrapfig = true
617
+ }
618
+ content += `${innerFigure}${caption.length ? `\\caption*{${caption}}` : ""}\\label{${node.attrs.id}}\n`
619
+ }
620
+ if (copyright?.holder) {
621
+ content += `% © ${copyright.year ? copyright.year : new Date().getFullYear()} ${copyright.holder}\n`
622
+ }
623
+ if (copyright?.licenses.length) {
624
+ copyright.licenses.forEach(license => {
625
+ content += `% ${license.title}: ${license.url}${license.start ? ` (${license.start})\n` : ""}\n`
626
+ })
627
+ }
628
+ if (this.internalLinks.includes(node.attrs.id)) {
629
+ // Add a link target
630
+ end =
631
+ `\\texorpdfstring{\\protect\\hypertarget{${node.attrs.id}}{}}{}\n` +
632
+ end
633
+ }
634
+ this.features.captions = true
635
+ break
636
+ }
637
+ case "figure_caption":
638
+ // We are already dealing with this in the figure. Prevent content from being added a second time.
639
+ return ""
640
+ case "figure_equation":
641
+ // We are already dealing with this in the figure.
642
+ break
643
+ case "image":
644
+ // We are already dealing with this in the figure.
645
+ break
646
+ case "table":
647
+ if (node.content?.length) {
648
+ const category = node.attrs.category
649
+
650
+ const captionContent = node.attrs.caption
651
+ ? node.content[0].content || []
652
+ : []
653
+ let caption
654
+ if (category !== "none") {
655
+ if (!this.categoryCounter[category]) {
656
+ this.categoryCounter[category] = 1
657
+ }
658
+ const catCount = this.categoryCounter[category]++
659
+ const catLabel = `${CATS[category][this.settings.language]} ${catCount}`
660
+ if (captionContent.length) {
661
+ caption = `${catLabel}: ${captionContent.map(node => this.walkJson(node)).join("")}`
662
+ } else {
663
+ caption = catLabel
664
+ }
665
+ } else {
666
+ caption = captionContent
667
+ .map(node => this.walkJson(node))
668
+ .join("")
669
+ }
670
+ let columns = 1
671
+ if (
672
+ node.content.length > 1 &&
673
+ node.content[1].content?.length
674
+ ) {
675
+ columns = node.content[1].content[0].content.reduce(
676
+ (columns, node) => columns + node.attrs.colspan,
677
+ 0
678
+ )
679
+ }
680
+ const aligned =
681
+ node.attrs.width === "100" ? "left" : node.attrs.aligned
682
+ if (aligned === "center") {
683
+ start += "\n\n\\begin{center}"
684
+ end = "\n\n\\end{center}\n" + end
685
+ } else if (aligned === "right") {
686
+ start += "\n\n{\\raggedleft" // This is not a typo - raggedleft = aligned: right
687
+ end = "\n\n}\n"
688
+ } // aligned === 'left' is default
689
+ if (caption.length) {
690
+ start += "\n\\begin{table}\n"
691
+ start += `\\caption*{${caption}}\\label{${node.attrs.id}}`
692
+ end = "\\end{table}\n" + end
693
+ this.features.captions = true
694
+ }
695
+ start += `\n\n\\begin{tabu} to ${
696
+ node.attrs.width === "100"
697
+ ? ""
698
+ : Number.parseInt(node.attrs.width) / 100
699
+ }\\textwidth { |${"X|".repeat(columns)} }\n\\hline\n\n`
700
+ end = "\\hline\n\n\\end{tabu}" + end
701
+ this.features.tables = true
702
+ }
703
+ break
704
+ case "table_body":
705
+ // Pass through to table.
706
+ break
707
+ case "table_caption":
708
+ // We already deal with this in 'table'.
709
+ return ""
710
+ case "table_row":
711
+ end += " \\\\\n"
712
+ break
713
+ case "table_cell":
714
+ case "table_header":
715
+ if (node.attrs.colspan > 1) {
716
+ start += `\\multicolumn{${node.attrs.colspan}}{c}{`
717
+ end += "}"
718
+ }
719
+ // TODO: these multirow outputs don't work very well with longer text.
720
+ // If there is another alternative, please change!
721
+ if (node.attrs.rowspan > 1) {
722
+ start += `\\multirow{${node.attrs.rowspan}}{*}{`
723
+ end += "}"
724
+ this.features.rowspan = true
725
+ }
726
+ end += " & "
727
+ break
728
+ case "equation":
729
+ content += `$${node.attrs.equation}$`
730
+ break
731
+ case "hard_break":
732
+ if (!options.noLineBreak) {
733
+ content += "\n\n"
734
+ }
735
+ break
736
+ default:
737
+ break
738
+ }
739
+
740
+ if (node.content) {
741
+ node.content.forEach(child => {
742
+ content += this.walkJson(child, options)
743
+ })
744
+ }
745
+ if (placeFootnotesAfterBlock && options.unplacedFootnotes?.length) {
746
+ // There are footnotes that needed to be placed behind the node.
747
+ // This happens in the case of headlines and lists.
748
+ end += `\\addtocounter{footnote}{-${options.unplacedFootnotes.length}}`
749
+ options.unplacedFootnotes.forEach(footnote => {
750
+ end += "\\stepcounter{footnote}\n"
751
+ end += "\\footnotetext{"
752
+ let fnContent = ""
753
+ footnote.forEach(footPar => {
754
+ fnContent += this.walkJson(footPar, options)
755
+ })
756
+ end += fnContent.replace(/^\s+|\s+$/g, "")
757
+ end += "}"
758
+ })
759
+ options.unplacedFootnotes = []
760
+ }
761
+ if (
762
+ ["table_cell", "table_header"].includes(node.type) &&
763
+ node.attrs.rowspan > 1
764
+ ) {
765
+ // \multirow doesn't allow multiple paragraphs.
766
+ content = content.trim().replace(/\n\n/g, " \\\\ ")
767
+ }
768
+
769
+ return start + content + end
770
+ }
771
+
772
+ // The database doesn't ensure that citation keys are unique.
773
+ // So here we need to make sure that the same key is not used twice in one
774
+ // document.
775
+ createUniqueCitationKey(suggestedKey) {
776
+ const usedKeys = Object.keys(this.usedBibDB).map(key => {
777
+ return this.usedBibDB[key].entry_key
778
+ })
779
+ if (usedKeys.includes(suggestedKey)) {
780
+ suggestedKey += "X"
781
+ return this.createUniqueCitationKey(suggestedKey)
782
+ } else {
783
+ return suggestedKey
784
+ }
785
+ }
786
+
787
+ postProcess(latex) {
788
+ return (
789
+ latex
790
+ // join blocks of the same type that follow oneanother.
791
+ .replace(/\\end{code}\n\n\\begin{code}\n\n/g, "")
792
+ .replace(/\\end{quote}\n\n\\begin{quote}\n\n/g, "")
793
+ // Remove the last divider in any any table row.
794
+ .replace(/& {2}\\\\/g, "\\\\")
795
+ // Remove new lines between table cells.
796
+ .replace(/\n & \n\n/g, " & ")
797
+ // Remove new lines within itemization
798
+ .replace(/\\item \n\n/g, "\\item ")
799
+ )
800
+ }
801
+
802
+ assembleEpilogue() {
803
+ let epilogue = ""
804
+ if (this.features.citations) {
805
+ const bibliographyHeader =
806
+ this.settings.bibliography_header[this.settings.language] ||
807
+ BIBLIOGRAPHY_HEADERS[this.settings.language]
808
+ epilogue += `\n\n\\printbibliography[title={${escapeLatexText(bibliographyHeader)}}]`
809
+ }
810
+ return epilogue
811
+ }
812
+
813
+ assembleCopyright() {
814
+ let note = ""
815
+ if (this.settings.copyright) {
816
+ if (this.settings.copyright.holder) {
817
+ note += `% © ${this.settings.copyright.year ? this.settings.copyright.year : new Date().getFullYear()} ${this.settings.copyright.holder}\n`
818
+ }
819
+ if (this.settings.copyright.licenses.length) {
820
+ this.settings.copyright.licenses.forEach(license => {
821
+ note += `% ${license.url}${license.start ? ` (${license.start})` : ""}\n`
822
+ })
823
+ }
824
+ }
825
+
826
+ if (note.length) {
827
+ note += "\n\n"
828
+ }
829
+ return note
830
+ }
831
+
832
+ assemblePreamble() {
833
+ let preamble = ""
834
+
835
+ if (this.features.subtitle) {
836
+ preamble += `
837
+ \n\\usepackage{titling}
838
+ \n\\newcommand{\\subtitle}[1]{%
839
+ \n\t\\posttitle{%
840
+ \n\t\t\\par\\end{center}
841
+ \n\t\t\\begin{center}\\large#1\\end{center}
842
+ \n\t\t\\vskip 0.5em}%
843
+ }
844
+ `
845
+ }
846
+ if (this.features.authors) {
847
+ preamble += `
848
+ \n\\usepackage{authblk}
849
+ \n\\makeatletter
850
+ \n\\let\\@fnsymbol\\@alph
851
+ \n\\makeatother
852
+ `
853
+ }
854
+
855
+ if (this.features.keywords) {
856
+ preamble += `
857
+ \n\\def\\keywords{\\vspace{.5em}
858
+ \n{\\textit{Keywords}:\\,\\relax%
859
+ \n}}
860
+ \n\\def\\endkeywords{\\par}
861
+ \n\\newcommand{\\sep}{, }
862
+ `
863
+ }
864
+
865
+ if (this.features.hyperlinks) {
866
+ preamble += "\n\\usepackage{hyperref}"
867
+ }
868
+
869
+ if (this.features.captions) {
870
+ preamble += "\n\\usepackage{caption}"
871
+ }
872
+
873
+ if (this.features.wrapfig) {
874
+ preamble += "\n\\usepackage{wrapfig}"
875
+ }
876
+
877
+ if (this.features.citations) {
878
+ preamble += `
879
+ \n\\usepackage[backend=biber,hyperref=false,citestyle=authoryear,bibstyle=authoryear]{biblatex}
880
+ \n\\bibliography{bibliography}
881
+ `
882
+ }
883
+
884
+ if (this.features.SVGs) {
885
+ preamble += "\n\\usepackage{svg}"
886
+ }
887
+
888
+ if (this.features.images) {
889
+ preamble += "\n\\usepackage{graphicx}"
890
+ // The following scales graphics down to text width, but not scaling them up if they are smaller
891
+ preamble += `
892
+ \n\\usepackage{calc}
893
+ \n\\newlength{\\imgwidth}
894
+ \n\\newcommand\\scaledgraphics[2]{%
895
+ \n\\settowidth{\\imgwidth}{\\includegraphics{#1}}%
896
+ \n\\setlength{\\imgwidth}{\\minof{\\imgwidth}{#2\\textwidth}}%
897
+ \n\\includegraphics[width=\\imgwidth,height=\\textheight,keepaspectratio]{#1}%
898
+ \n}
899
+ `
900
+ }
901
+
902
+ if (this.features.tables) {
903
+ preamble += "\n\\usepackage{tabu}"
904
+ }
905
+
906
+ if (this.features.orderedListStart) {
907
+ preamble += "\n\\usepackage{enumitem}"
908
+ }
909
+
910
+ if (this.features.rowspan) {
911
+ preamble += "\n\\usepackage{multirow}"
912
+ }
913
+
914
+ if (this.features.code) {
915
+ // See https://tex.stackexchange.com/questions/445424/making-a-multiline-code-environment
916
+ preamble += `
917
+ \n\\usepackage{xcolor}
918
+ \\definecolor{mygray}{gray}{0.9}
919
+ \\usepackage{fvextra}
920
+ \\usepackage{tcolorbox}
921
+ \\newenvironment{code}%
922
+ {\\VerbatimEnvironment
923
+ \\begin{tcolorbox}[colback=mygray, boxsep=0pt, arc=0pt, boxrule=0pt]
924
+ \\begin{Verbatim}[fontsize=\\scriptsize, commandchars=\\\\\\{\\},
925
+ breaklines, breakafter=*, breaksymbolsep=0.5em,
926
+ breakaftersymbolpre={\\,\\tiny\\ensuremath{\\rfloor}}]}%
927
+ {\\end{Verbatim}%
928
+ \\end{tcolorbox}}
929
+ `
930
+ }
931
+
932
+ return preamble
933
+ }
934
+ }