@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.
- package/LICENSE +661 -0
- package/README.md +16 -0
- package/jest.config.js +23 -0
- package/package.json +59 -0
- package/schema.json +1 -0
- package/scripts/export-schema.js +16 -0
- package/src/bibliography/common.js +92 -0
- package/src/bibliography/csl_bib.js +139 -0
- package/src/citations/citeproc_sys.js +42 -0
- package/src/citations/format.js +194 -0
- package/src/common/blob.js +10 -0
- package/src/common/file.js +25 -0
- package/src/common/index.js +12 -0
- package/src/common/network.js +79 -0
- package/src/common/text.js +44 -0
- package/src/editor/e2ee/encryptor.js +228 -0
- package/src/exporter/docx/citations.js +177 -0
- package/src/exporter/docx/comments.js +165 -0
- package/src/exporter/docx/footnotes.js +240 -0
- package/src/exporter/docx/images.js +101 -0
- package/src/exporter/docx/index.js +185 -0
- package/src/exporter/docx/lists.js +260 -0
- package/src/exporter/docx/math.js +46 -0
- package/src/exporter/docx/metadata.js +289 -0
- package/src/exporter/docx/rels.js +193 -0
- package/src/exporter/docx/render.js +941 -0
- package/src/exporter/docx/richtext.js +1182 -0
- package/src/exporter/docx/tables.js +112 -0
- package/src/exporter/docx/tools.js +50 -0
- package/src/exporter/epub/index.js +142 -0
- package/src/exporter/epub/templates.js +140 -0
- package/src/exporter/epub/tools.js +96 -0
- package/src/exporter/html/citations.js +121 -0
- package/src/exporter/html/convert.js +813 -0
- package/src/exporter/html/index.js +192 -0
- package/src/exporter/html/templates.js +34 -0
- package/src/exporter/html/tools.js +50 -0
- package/src/exporter/jats/bibliography.js +183 -0
- package/src/exporter/jats/citations.js +109 -0
- package/src/exporter/jats/convert.js +871 -0
- package/src/exporter/jats/index.js +92 -0
- package/src/exporter/jats/templates.js +35 -0
- package/src/exporter/jats/text.js +72 -0
- package/src/exporter/latex/convert.js +934 -0
- package/src/exporter/latex/escape_latex.js +21 -0
- package/src/exporter/latex/index.js +74 -0
- package/src/exporter/latex/readme.js +22 -0
- package/src/exporter/native/shrink.js +132 -0
- package/src/exporter/odt/citations.js +101 -0
- package/src/exporter/odt/footnotes.js +147 -0
- package/src/exporter/odt/images.js +115 -0
- package/src/exporter/odt/index.js +156 -0
- package/src/exporter/odt/math.js +57 -0
- package/src/exporter/odt/metadata.js +251 -0
- package/src/exporter/odt/render.js +806 -0
- package/src/exporter/odt/richtext.js +865 -0
- package/src/exporter/odt/styles.js +387 -0
- package/src/exporter/odt/track.js +68 -0
- package/src/exporter/pandoc/citations.js +98 -0
- package/src/exporter/pandoc/convert.js +1017 -0
- package/src/exporter/pandoc/index.js +92 -0
- package/src/exporter/pandoc/readme.js +8 -0
- package/src/exporter/pandoc/tools.js +51 -0
- package/src/exporter/print/index.js +177 -0
- package/src/exporter/tools/doc_content.js +144 -0
- package/src/exporter/tools/file.js +9 -0
- package/src/exporter/tools/json.js +73 -0
- package/src/exporter/tools/svg.js +29 -0
- package/src/exporter/tools/xml.js +531 -0
- package/src/exporter/tools/xml_zip.js +95 -0
- package/src/exporter/tools/zip.js +90 -0
- package/src/exporter/tools/zotero_csl.js +93 -0
- package/src/importer/citations.js +129 -0
- package/src/importer/docx/citations.js +123 -0
- package/src/importer/docx/convert.js +1427 -0
- package/src/importer/docx/helpers.js +9 -0
- package/src/importer/docx/omml2mathml.js +1448 -0
- package/src/importer/docx/parse.js +735 -0
- package/src/importer/native/get_images.js +76 -0
- package/src/importer/native/update.js +29 -0
- package/src/importer/odt/citations.js +87 -0
- package/src/importer/odt/convert.js +1855 -0
- package/src/importer/pandoc/convert.js +884 -0
- package/src/importer/pandoc/helpers.js +84 -0
- package/src/importer/zip_analyzer.js +102 -0
- package/src/index.js +1 -0
- package/src/mathlive/opf_includes.js +24 -0
- package/src/schema/common/annotate.js +76 -0
- package/src/schema/common/base.js +118 -0
- package/src/schema/common/citation.js +62 -0
- package/src/schema/common/equation.js +31 -0
- package/src/schema/common/figure.js +190 -0
- package/src/schema/common/heading.js +43 -0
- package/src/schema/common/index.js +40 -0
- package/src/schema/common/list.js +95 -0
- package/src/schema/common/reference.js +100 -0
- package/src/schema/common/table.js +103 -0
- package/src/schema/common/track.js +190 -0
- package/src/schema/const.js +58 -0
- package/src/schema/convert.js +1272 -0
- package/src/schema/document/content.js +187 -0
- package/src/schema/document/index.js +117 -0
- package/src/schema/document/structure.js +452 -0
- package/src/schema/export.js +21 -0
- package/src/schema/footnotes.js +126 -0
- package/src/schema/footnotes_convert.js +31 -0
- package/src/schema/i18n.js +595 -0
- package/src/schema/index.js +5 -0
- package/src/schema/mini_json.js +61 -0
- 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
|
+
}
|