@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,1182 @@
1
+ import {escapeText} from "../../common/index.js"
2
+ import {CATS} from "../../schema/i18n.js"
3
+
4
+ import {xmlDOM} from "../tools/xml.js"
5
+ import {createZoteroCitation} from "../tools/zotero_csl.js"
6
+
7
+ import {translateBlockType} from "./tools.js"
8
+
9
+ const TEXT_BLOCK_TYPES = [
10
+ "heading1",
11
+ "heading2",
12
+ "heading3",
13
+ "heading4",
14
+ "heading5",
15
+ "heading6",
16
+ "paragraph",
17
+ "code_block"
18
+ ]
19
+
20
+ const INLINE_TYPES = [
21
+ "citation",
22
+ "cross_reference",
23
+ "cslbib",
24
+ "cslblock",
25
+ "cslindent",
26
+ "cslinline",
27
+ "cslleftmargin",
28
+ "cslrightinline",
29
+ "equation",
30
+ "footnote",
31
+ "hard_break",
32
+ "image",
33
+ "text"
34
+ ]
35
+
36
+ /**
37
+ * Create Zotero citation field instruction for DOCX.
38
+ * @param {Array} references - Array of {id, prefix?, locator?} from citation node
39
+ * @param {Object} bibDB - Bibliography database
40
+ * @param {string} formattedCitation - Pre-formatted citation text from citeproc
41
+ * @param {string} citationId - Optional citation ID (generated if not provided)
42
+ * @returns {string} Field instruction text
43
+ */
44
+
45
+ function createZoteroCitationField(
46
+ references,
47
+ bibDB,
48
+ formattedCitation,
49
+ citationId = null
50
+ ) {
51
+ const zoteroCitation = createZoteroCitation(
52
+ references,
53
+ bibDB,
54
+ formattedCitation,
55
+ citationId
56
+ )
57
+ if (!zoteroCitation) {
58
+ return null
59
+ }
60
+ const jsonStr = JSON.stringify(zoteroCitation)
61
+ return ` ADDIN ZOTERO_ITEM CSL_CITATION${jsonStr} `
62
+ }
63
+
64
+ export class DOCXExporterRichtext {
65
+ constructor(
66
+ doc,
67
+ settings,
68
+ lists,
69
+ footnotes,
70
+ math,
71
+ tables,
72
+ rels,
73
+ citations,
74
+ images
75
+ ) {
76
+ this.doc = doc
77
+ this.settings = settings
78
+ this.lists = lists
79
+ this.footnotes = footnotes
80
+ this.math = math
81
+ this.tables = tables
82
+ this.rels = rels
83
+ this.citations = citations
84
+ this.images = images
85
+
86
+ this.comments = {}
87
+ this.commentRangeCounter = -1
88
+ this.changeCounter = 0
89
+ this.fnCounter = 1 // footnotes 0 and 1 are occupied by separators by default.
90
+ this.bookmarkCounter = -1
91
+ this.categoryCounter = {} // counters for each type of figure (figure/table/photo)
92
+ this.fncategoryCounter = {}
93
+ this.docPrCount = -1
94
+ this.citationCounter = 0 // Track which citation we're processing
95
+ }
96
+
97
+ run(node, options = {}, nextNode = null) {
98
+ options.comments = this.findComments(node) // Data related to comments. We need to mark the first and last occurence of comment
99
+ return this.transformRichtext(node, options, nextNode)
100
+ }
101
+
102
+ findComments(node, comments = {}) {
103
+ if (node.marks) {
104
+ node.marks
105
+ .filter(mark => mark.type === "comment")
106
+ .forEach(comment => {
107
+ if (!this.doc.comments[comment.attrs.id]) {
108
+ return
109
+ }
110
+ if (!comments[comment.attrs.id]) {
111
+ comments[comment.attrs.id] = {
112
+ start: node,
113
+ end: node,
114
+ content: this.doc.comments[comment.attrs.id]
115
+ }
116
+ } else {
117
+ comments[comment.attrs.id]["end"] = node
118
+ }
119
+ })
120
+ }
121
+ if (node.content) {
122
+ for (let i = 0; i < node.content.length; i++) {
123
+ this.findComments(node.content[i], comments)
124
+ }
125
+ }
126
+ return comments
127
+ }
128
+
129
+ transformRichtext(node, options = {}, nextNode = null) {
130
+ let start = "",
131
+ content = "",
132
+ end = ""
133
+
134
+ if (node.marks && options.comments) {
135
+ // Footnotes don't allow comments in DOCX
136
+ node.marks
137
+ .filter(mark => mark.type === "comment")
138
+ .forEach(comment => {
139
+ const commentData = options.comments[comment.attrs.id]
140
+ if (!commentData) {
141
+ return
142
+ }
143
+ if (commentData.start === node) {
144
+ let commentId = this.comments[comment.attrs.id]
145
+ start += `<w:commentRangeStart w:id="${commentId}"/>`
146
+ commentData.content.answers?.forEach(
147
+ _answer =>
148
+ (start += `<w:commentRangeStart w:id="${++commentId}"/>`)
149
+ )
150
+ }
151
+
152
+ if (commentData.end === node) {
153
+ let commentId = this.comments[comment.attrs.id]
154
+ end =
155
+ `<w:commentRangeEnd w:id="${commentId}"/><w:r><w:commentReference w:id="${
156
+ commentId
157
+ }"/></w:r>${(commentData.content.answers || [])
158
+ .map(
159
+ _answer =>
160
+ `<w:commentRangeEnd w:id="${++commentId}"/><w:r><w:commentReference w:id="${commentId}"/></w:r>`
161
+ )
162
+ .join("")}` + end
163
+ }
164
+ })
165
+ }
166
+
167
+ const inlineType = INLINE_TYPES.includes(node.type)
168
+
169
+ let inlineDelete,
170
+ nextBlockDelete,
171
+ nextBlockInsert,
172
+ blockChange,
173
+ blockDelete,
174
+ blockInsert
175
+ if (inlineType) {
176
+ const inlineInsert =
177
+ inlineType &&
178
+ (node.marks?.find(
179
+ mark =>
180
+ mark.type === "insertion" &&
181
+ mark.attrs.approved === false
182
+ )?.attrs ||
183
+ options.blockInsert)
184
+ inlineDelete =
185
+ inlineType &&
186
+ (node.marks?.find(mark => mark.type === "deletion")?.attrs ||
187
+ options.blockDelete)
188
+ if (
189
+ inlineInsert &&
190
+ inlineDelete &&
191
+ inlineInsert.username === inlineDelete.username
192
+ ) {
193
+ // In DOCX, the same user cannot both have a pending insertion and deletion of the same inline content. We remove it.
194
+ return ""
195
+ } else {
196
+ if (inlineInsert) {
197
+ start += `<w:ins w:id="${++this.changeCounter}" w:author="${escapeText(inlineInsert.username)}" w:date="${new Date(inlineInsert.date * 60000).toISOString().split(".")[0]}Z">`
198
+ end = "</w:ins>" + end
199
+ }
200
+ if (inlineDelete) {
201
+ start += `<w:del w:id="${++this.changeCounter}" w:author="${escapeText(inlineDelete.username)}" w:date="${new Date(inlineDelete.date * 60000).toISOString().split(".")[0]}Z">`
202
+ end = "</w:del>" + end
203
+ }
204
+ }
205
+ } else if (TEXT_BLOCK_TYPES.includes(node.type)) {
206
+ blockChange = node.attrs?.track?.find(
207
+ mark => mark.type === "block_change"
208
+ )
209
+
210
+ if (nextNode && TEXT_BLOCK_TYPES.includes(nextNode.type)) {
211
+ nextBlockDelete = nextNode.attrs?.track?.find(
212
+ mark => mark.type === "deletion"
213
+ )
214
+ nextBlockInsert = nextNode.attrs?.track?.find(
215
+ mark => mark.type === "insertion"
216
+ )
217
+ }
218
+ } else {
219
+ blockDelete = node.attrs?.track?.find(
220
+ mark => mark.type === "deletion"
221
+ )
222
+ if (blockDelete) {
223
+ options = Object.assign({}, options)
224
+ options.blockDelete = blockDelete
225
+ }
226
+ blockInsert = node.attrs?.track?.find(
227
+ mark => mark.type === "insertion"
228
+ )
229
+ if (blockInsert) {
230
+ options = Object.assign({}, options)
231
+ options.blockInsert = blockInsert
232
+ }
233
+ }
234
+ switch (node.type) {
235
+ case "doc":
236
+ // We handle the contents directly
237
+ break
238
+ case "paragraph":
239
+ if (!options.section) {
240
+ options.section = "Normal"
241
+ }
242
+ // This should really be something like
243
+ // '<w:p w:rsidR="A437D321" w:rsidRDefault="2B935ADC">'
244
+ // See: https://blogs.msdn.microsoft.com/brian_jones/2006/12/11/whats-up-with-all-those-rsids/
245
+ // But tests with Word 2016/LibreOffice seem to indicate that it
246
+ // doesn't care if the attributes are missing.
247
+ // We may need to add them later, if it turns out this is a problem
248
+ // for other versions of Word. In that case we should also add
249
+ // it to settings.xml as described in above link.
250
+ if (
251
+ options.section === "Normal" &&
252
+ !options.list_type &&
253
+ !node.content?.length
254
+ ) {
255
+ start += "<w:p/>"
256
+ } else {
257
+ start += `
258
+ <w:p${options.paragraphId ? ` w14:paraId="${options.paragraphId}"` : ""}>
259
+ <w:pPr><w:pStyle w:val="${options.section}"/>`
260
+ if (options.list_type) {
261
+ start += `<w:numPr><w:ilvl w:val="${options.list_depth}"/>`
262
+ start += `<w:numId w:val="${options.list_type}"/></w:numPr>`
263
+ } else {
264
+ start += `
265
+ <w:rPr>
266
+ ${
267
+ nextBlockInsert
268
+ ? `<w:ins w:id="${++this.changeCounter}" w:author="${escapeText(nextBlockInsert.username)}" w:date="${new Date(nextBlockInsert.date * 60000).toISOString().split(".")[0]}Z"/>`
269
+ : ""
270
+ }
271
+ ${
272
+ nextBlockDelete
273
+ ? `<w:del w:id="${++this.changeCounter}" w:author="${escapeText(nextBlockDelete.username)}" w:date="${new Date(nextBlockDelete.date * 60000).toISOString().split(".")[0]}Z"/>`
274
+ : ""
275
+ }
276
+ </w:rPr>`
277
+ }
278
+ if (blockChange) {
279
+ start += `
280
+ <w:pPrChange w:id="${++this.changeCounter}" w:author="${escapeText(blockChange.username)}" w:date="${new Date(blockChange.date * 60000).toISOString().split(".")[0]}Z">
281
+ <w:pPr>
282
+ <w:pStyle w:val="${translateBlockType(blockChange.before.type)}"/>
283
+ </w:pPr>
284
+ </w:pPrChange>`
285
+ }
286
+ start += "</w:pPr>"
287
+ end = "</w:p>" + end
288
+ if (!node.content?.length) {
289
+ start += "<w:r><w:rPr></w:rPr></w:r>"
290
+ }
291
+ }
292
+ if (options.commentReference) {
293
+ end =
294
+ '<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:annotationRef/></w:r>' +
295
+ end
296
+ options = Object.assign({}, options)
297
+ options.commentReference = false
298
+ }
299
+ break
300
+ case "bibliography_heading":
301
+ start += `
302
+ <w:p>
303
+ <w:pPr>
304
+ <w:pStyle w:val="BibliographyHeading"/>
305
+ <w:rPr></w:rPr>
306
+ </w:pPr>`
307
+ end = "</w:p>" + end
308
+ break
309
+ case "heading1":
310
+ case "heading2":
311
+ case "heading3":
312
+ case "heading4":
313
+ case "heading5":
314
+ case "heading6":
315
+ start += `
316
+ <w:p>
317
+ <w:pPr>
318
+ <w:pStyle w:val="${translateBlockType(node.type)}"/>
319
+ <w:rPr>
320
+ ${
321
+ nextBlockInsert
322
+ ? `<w:ins w:id="${++this.changeCounter}" w:author="${escapeText(nextBlockInsert.username)}" w:date="${new Date(nextBlockInsert.date * 60000).toISOString().split(".")[0]}Z"/>`
323
+ : ""
324
+ }
325
+ ${
326
+ nextBlockDelete
327
+ ? `<w:del w:id="${++this.changeCounter}" w:author="${escapeText(nextBlockDelete.username)}" w:date="${new Date(nextBlockDelete.date * 60000).toISOString().split(".")[0]}Z"/>`
328
+ : ""
329
+ }
330
+ </w:rPr>
331
+ ${
332
+ blockChange
333
+ ? blockChange.before.type === "paragraph"
334
+ ? `<w:pPrChange w:id="${++this.changeCounter}" w:author="${escapeText(blockChange.username)}" w:date="${new Date(blockChange.date * 60000).toISOString().split(".")[0]}Z"/>`
335
+ : `<w:pPrChange w:id="${++this.changeCounter}" w:author="${escapeText(blockChange.username)}" w:date="${new Date(blockChange.date * 60000).toISOString().split(".")[0]}Z">
336
+ <w:pPr>
337
+ <w:pStyle w:val="${translateBlockType(blockChange.before.type)}"/>
338
+ </w:pPr>
339
+ </w:pPrChange>`
340
+ : ""
341
+ }
342
+ </w:pPr>
343
+ <w:bookmarkStart w:name="${node.attrs.id}" w:id="${++this.bookmarkCounter}"/>
344
+ <w:bookmarkEnd w:id="${this.bookmarkCounter}"/>`
345
+ end = "</w:p>" + end
346
+ break
347
+ case "blockquote":
348
+ // This is imperfect, but Word doesn't seem to provide section/quotation nesting
349
+ // Also, track information on wrapping into blockquote is not exported.
350
+ options = Object.assign({}, options)
351
+ options.section = "Quote"
352
+ break
353
+ case "code_block": {
354
+ // Handle code blocks with category support
355
+ const category = node.attrs.category
356
+ let categoryLabel = ""
357
+
358
+ if (category && node.attrs.id) {
359
+ const categoryCounter = options.inFootnote
360
+ ? this.fnCategoryCounter
361
+ : this.categoryCounter
362
+ if (!categoryCounter[category]) {
363
+ categoryCounter[category] = 1
364
+ }
365
+ const catCount = categoryCounter[category]++
366
+ const {CATS} = require("../../schema/i18n")
367
+ const categoryLabelText =
368
+ CATS[category]?.[this.settings.language] || category
369
+ const title = node.attrs.title
370
+ ? `: ${escapeText(node.attrs.title)}`
371
+ : ""
372
+
373
+ // Create category label paragraph with SEQ field for numbering
374
+ categoryLabel = `
375
+ <w:p>
376
+ <w:pPr><w:pStyle w:val="Caption"/></w:pPr>
377
+ <w:bookmarkStart w:name="${node.attrs.id}" w:id="${++this.bookmarkCounter}"/>
378
+ <w:r>
379
+ <w:t xml:space="preserve">${categoryLabelText} </w:t>
380
+ </w:r>
381
+ <w:fldSimple w:instr=" SEQ ${category} \\* ARABIC ">
382
+ <w:r>
383
+ <w:t>${catCount}${options.inFootnote ? "A" : ""}</w:t>
384
+ </w:r>
385
+ </w:fldSimple>
386
+ <w:r>
387
+ <w:t xml:space="preserve">${title}</w:t>
388
+ </w:r>
389
+ <w:bookmarkEnd w:id="${this.bookmarkCounter}"/>
390
+ </w:p>`
391
+ }
392
+
393
+ if (!node.content?.length) {
394
+ start += categoryLabel + "<w:p/>"
395
+ } else {
396
+ options = Object.assign({}, options)
397
+ options.section = "Code"
398
+ start +=
399
+ categoryLabel +
400
+ `
401
+ <w:p${options.paragraphId ? ` w14:paraId="${options.paragraphId}"` : ""}>
402
+ <w:pPr><w:pStyle w:val="${options.section}"/>`
403
+ if (options.list_type) {
404
+ start += `<w:numPr><w:ilvl w:val="${options.list_depth}"/>`
405
+ start += `<w:numId w:val="${options.list_type}"/></w:numPr>`
406
+ } else {
407
+ start += `
408
+ <w:rPr>
409
+ ${
410
+ nextBlockInsert
411
+ ? `<w:ins w:id="${++this.changeCounter}" w:author="${escapeText(nextBlockInsert.username)}" w:date="${new Date(nextBlockInsert.date * 60000).toISOString().split(".")[0]}Z"/>`
412
+ : ""
413
+ }
414
+ ${
415
+ nextBlockDelete
416
+ ? `<w:del w:id="${++this.changeCounter}" w:author="${escapeText(nextBlockDelete.username)}" w:date="${new Date(nextBlockDelete.date * 60000).toISOString().split(".")[0]}Z"/>`
417
+ : ""
418
+ }
419
+ </w:rPr>`
420
+ }
421
+ if (blockChange) {
422
+ start += `
423
+ <w:pPrChange w:id="${++this.changeCounter}" w:author="${escapeText(blockChange.username)}" w:date="${new Date(blockChange.date * 60000).toISOString().split(".")[0]}Z">
424
+ <w:pPr>
425
+ <w:pStyle w:val="${translateBlockType(blockChange.before.type)}"/>
426
+ </w:pPr>
427
+ </w:pPrChange>`
428
+ }
429
+ start += "</w:pPr>"
430
+ end = "</w:p>" + end
431
+ if (!node.content?.length) {
432
+ start += "<w:r><w:rPr></w:rPr></w:r>"
433
+ }
434
+ }
435
+ if (options.commentReference) {
436
+ end =
437
+ '<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:annotationRef/></w:r>' +
438
+ end
439
+ options.commentReference = false
440
+ }
441
+ break
442
+ }
443
+ case "ordered_list": {
444
+ options = Object.assign({}, options)
445
+ options.section = "ListParagraph"
446
+ if (options.list_depth === undefined) {
447
+ options.list_depth = 0
448
+ } else {
449
+ options.list_depth += 1
450
+ }
451
+ options.list_type = this.lists.getNumberedType()
452
+ break
453
+ }
454
+ case "bullet_list":
455
+ options = Object.assign({}, options)
456
+ options.section = "ListParagraph"
457
+ options.list_type = this.lists.getBulletType()
458
+ if (options.list_depth === undefined) {
459
+ options.list_depth = 0
460
+ } else {
461
+ options.list_depth += 1
462
+ }
463
+ break
464
+ case "list_item":
465
+ // Word seems to lack complex nesting options. The styling is applied
466
+ // to child paragraphs. This will deliver correct results in most
467
+ // cases.
468
+ break
469
+ case "footnotecontainer":
470
+ options = Object.assign({}, options)
471
+ options.section = "Footnote"
472
+ options.inFootnote = true
473
+ start += `<w:footnote w:id="${++this.fnCounter}">`
474
+ end = "</w:footnote>" + end
475
+ options.footnoteRefMissing = true
476
+ break
477
+ case "footnote":
478
+ content += `
479
+ <w:r>
480
+ <w:rPr>
481
+ <w:rStyle w:val="FootnoteAnchor"/>
482
+ </w:rPr>
483
+ <w:footnoteReference w:id="${++this.fnCounter}"/>
484
+ </w:r>`
485
+ break
486
+ case "text": {
487
+ let hyperlink,
488
+ anchor,
489
+ em,
490
+ strong,
491
+ underline,
492
+ smallcaps,
493
+ sup,
494
+ sub,
495
+ code,
496
+ formatChange
497
+ // Check for hyperlink, anchor, bold/strong and italic/em
498
+ if (node.marks) {
499
+ hyperlink = node.marks.find(mark => mark.type === "link")
500
+ anchor = node.marks.find(mark => mark.type === "anchor")
501
+ em = node.marks.find(mark => mark.type === "em")
502
+ strong = node.marks.find(mark => mark.type === "strong")
503
+ underline = node.marks.find(
504
+ mark => mark.type === "underline"
505
+ )
506
+ smallcaps = node.marks.find(
507
+ mark => mark.type === "smallcaps"
508
+ )
509
+ sup = node.marks.find(mark => mark.type === "sup")
510
+ sub = node.marks.find(mark => mark.type === "sub")
511
+ code = node.marks.find(mark => mark.type === "code")
512
+ formatChange = node.marks.find(
513
+ mark => mark.type === "format_change"
514
+ )
515
+ }
516
+ if (anchor) {
517
+ start += `<w:bookmarkStart w:name="${anchor.attrs.id}" w:id="${++this.bookmarkCounter}"/><w:bookmarkEnd w:id="${this.bookmarkCounter}"/>`
518
+ end =
519
+ `<w:bookmarkStart w:name="${anchor.attrs.id}" w:id="${++this.bookmarkCounter}"/><w:bookmarkEnd w:id="${this.bookmarkCounter}"/>` +
520
+ end
521
+ }
522
+ if (hyperlink) {
523
+ const href = hyperlink.attrs.href
524
+ if (href[0] === "#") {
525
+ // Internal link
526
+ start += `<w:hyperlink w:anchor="${href.slice(1)}">`
527
+ } else {
528
+ // External link
529
+ const refId = this.rels.addLinkRel(href)
530
+ start += `<w:hyperlink r:id="rId${refId}">`
531
+ }
532
+ start += "<w:r>"
533
+ end = "</w:r></w:hyperlink>" + end
534
+ } else {
535
+ start += "<w:r>"
536
+ end = "</w:r>" + end
537
+ }
538
+ start += "<w:rPr>"
539
+ if (
540
+ hyperlink ||
541
+ em ||
542
+ strong ||
543
+ underline ||
544
+ smallcaps ||
545
+ sup ||
546
+ sub ||
547
+ code
548
+ ) {
549
+ if (hyperlink) {
550
+ this.rels.addLinkStyle()
551
+ start += `<w:rStyle w:val="${this.rels.hyperLinkStyle}"/>`
552
+ }
553
+ if (em) {
554
+ start += "<w:i/><w:iCs/>"
555
+ }
556
+ if (strong) {
557
+ start += "<w:b/><w:bCs/>"
558
+ }
559
+ if (underline) {
560
+ start += '<w:u w:val="single"/>'
561
+ }
562
+ if (smallcaps) {
563
+ start += "<w:smallCaps/>"
564
+ }
565
+ if (sup) {
566
+ start += '<w:vertAlign w:val="superscript"/>'
567
+ } else if (sub) {
568
+ start += '<w:vertAlign w:val="subscript"/>'
569
+ }
570
+ if (code) {
571
+ start +=
572
+ '<w:rFonts w:ascii="Courier New" w:hAnsi="Courier New"/>'
573
+ }
574
+ }
575
+ if (formatChange) {
576
+ const beforeStyle = formatChange.attrs.before
577
+ start += `<w:rPrChange w:id="${++this.changeCounter}" w:author="${escapeText(formatChange.attrs.username)}" w:date="${new Date(formatChange.attrs.date * 60000).toISOString().split(".")[0]}Z"><w:rPr>`
578
+ if (beforeStyle.includes("em")) {
579
+ start += "<w:i/><w:iCs/>"
580
+ }
581
+ if (beforeStyle.includes("strong")) {
582
+ start += "<w:b/><w:bCs/>"
583
+ }
584
+ if (beforeStyle.includes("underline")) {
585
+ start += '<w:u w:val="single"/>'
586
+ }
587
+ start += "</w:rPr></w:rPrChange>"
588
+ }
589
+ start += "</w:rPr>"
590
+ if (options.footnoteRefMissing) {
591
+ start += "<w:footnoteRef /><w:tab />"
592
+ options.footnoteRefMissing = false
593
+ }
594
+ let textAttr = ""
595
+ if (
596
+ node.text[0] === " " ||
597
+ node.text[node.text.length - 1] === " "
598
+ ) {
599
+ textAttr += ' xml:space="preserve"'
600
+ }
601
+ if (inlineDelete) {
602
+ start += `<w:delText${textAttr}>`
603
+ end = "</w:delText>" + end
604
+ } else {
605
+ start += `<w:t${textAttr}>`
606
+ end = "</w:t>" + end
607
+ }
608
+ content += escapeText(node.text)
609
+ break
610
+ }
611
+ case "cross_reference": {
612
+ const title = node.attrs.title
613
+ const id = node.attrs.id
614
+ let marks = node.marks.slice()
615
+ if (title && id) {
616
+ const hyperlink = {
617
+ type: "link",
618
+ attrs: {href: `#${id}`, title}
619
+ }
620
+ marks = marks.filter(mark => mark.type !== "link")
621
+ marks.push(hyperlink)
622
+ }
623
+ content += this.transformRichtext(
624
+ {
625
+ type: "text",
626
+ text: title || "MISSING TARGET",
627
+ marks
628
+ },
629
+ options,
630
+ nextNode
631
+ )
632
+ break
633
+ }
634
+ case "citation": {
635
+ // We take the first citation from the stack and remove it.
636
+ const cit = this.citations.pmCits.shift()
637
+
638
+ // Get citation info and formatted text for Zotero export
639
+ const citInfo = this.citations.citInfos[this.citationCounter]
640
+ const formattedText =
641
+ this.citations.citationTexts[this.citationCounter]
642
+ this.citationCounter++
643
+
644
+ // Create Zotero citation data on-the-fly
645
+ const fieldInstruction =
646
+ citInfo && formattedText
647
+ ? createZoteroCitationField(
648
+ citInfo.references,
649
+ this.citations.bibDB,
650
+ formattedText
651
+ )
652
+ : null
653
+
654
+ if (options.citationType === "note" && !options.inFootnote) {
655
+ // If the citations are in notes (footnotes), we need to
656
+ // put the content of this citation in a footnote.
657
+ // We then add the footnote to the footnote file and
658
+ // adjust the ids of all subsequent footnotes to be one higher
659
+ // than what they were until now.
660
+ content += `
661
+ <w:r>
662
+ <w:rPr>
663
+ <w:rStyle w:val="FootnoteAnchor"/>
664
+ </w:rPr>
665
+ <w:footnoteReference w:id="${this.fnCounter}"/>
666
+ </w:r>`
667
+
668
+ // Create footnote with Zotero field if available
669
+ let fnXML
670
+ if (fieldInstruction && formattedText) {
671
+ fnXML = `<w:footnote w:id="${this.fnCounter}">
672
+ <w:p>
673
+ <w:r>
674
+ <w:fldChar w:fldCharType="begin"/>
675
+ </w:r>
676
+ <w:r>
677
+ <w:instrText xml:space="preserve">${fieldInstruction}</w:instrText>
678
+ </w:r>
679
+ <w:r>
680
+ <w:fldChar w:fldCharType="separate"/>
681
+ </w:r>
682
+ <w:r>
683
+ <w:t>${formattedText}</w:t>
684
+ </w:r>
685
+ <w:r>
686
+ <w:fldChar w:fldCharType="end"/>
687
+ </w:r>
688
+ </w:p>
689
+ </w:footnote>`
690
+ } else {
691
+ const fnContents = this.transformRichtext(cit, {
692
+ footnoteRefMissing: true,
693
+ section: "Footnote"
694
+ })
695
+ fnXML = `<w:footnote w:id="${this.fnCounter}">${fnContents}</w:footnote>`
696
+ }
697
+
698
+ const xml = this.footnotes.xml
699
+ const lastId = this.fnCounter - 1
700
+ const footnotes = xml.queryAll("w:footnote")
701
+ footnotes.forEach(footnote => {
702
+ const id = Number.parseInt(
703
+ footnote.getAttribute("w:id")
704
+ )
705
+ if (id >= this.fnCounter) {
706
+ footnote.setAttribute("w:id", id + 1)
707
+ }
708
+ if (id === lastId) {
709
+ footnote.parentElement.insertBefore(
710
+ xmlDOM(fnXML),
711
+ footnote.nextSibling
712
+ )
713
+ }
714
+ })
715
+ this.fnCounter++
716
+ } else {
717
+ // In-text citation - create Zotero field if available
718
+ if (fieldInstruction && formattedText) {
719
+ content += `
720
+ <w:r>
721
+ <w:fldChar w:fldCharType="begin"/>
722
+ </w:r>
723
+ <w:r>
724
+ <w:instrText xml:space="preserve">${fieldInstruction}</w:instrText>
725
+ </w:r>
726
+ <w:r>
727
+ <w:fldChar w:fldCharType="separate"/>
728
+ </w:r>
729
+ <w:r>
730
+ <w:t>${formattedText}</w:t>
731
+ </w:r>
732
+ <w:r>
733
+ <w:fldChar w:fldCharType="end"/>
734
+ </w:r>`
735
+ } else {
736
+ // Fallback to formatted text only
737
+ for (let i = 0; i < cit.content.length; i++) {
738
+ content += this.transformRichtext(
739
+ cit.content[i],
740
+ options,
741
+ cit.content[i + 1]
742
+ )
743
+ }
744
+ }
745
+ }
746
+ break
747
+ }
748
+ case "figure": {
749
+ const category = node.attrs.category
750
+ let caption = node.attrs.caption
751
+ ? node.content.find(node => node.type === "figure_caption")
752
+ ?.content || []
753
+ : []
754
+ let catCountXML = ""
755
+ if (category !== "none") {
756
+ const categoryCounter = options.inFootnote
757
+ ? this.fncategoryCounter
758
+ : this.categoryCounter
759
+ if (!categoryCounter[category]) {
760
+ categoryCounter[category] = 1
761
+ }
762
+ catCountXML = `<w:r>
763
+ <w:t xml:space="preserve">${CATS[category][this.settings.language]} </w:t>
764
+ </w:r>
765
+ <w:r>
766
+ <w:rPr></w:rPr>
767
+ <w:fldChar w:fldCharType="begin"></w:fldChar>
768
+ </w:r>
769
+ <w:r>
770
+ <w:rPr></w:rPr>
771
+ <w:instrText> SEQ ${category} \\* ARABIC </w:instrText>
772
+ </w:r>
773
+ <w:r>
774
+ <w:rPr></w:rPr>
775
+ <w:fldChar w:fldCharType="separate" />
776
+ </w:r>
777
+ <w:r>
778
+ <w:rPr></w:rPr>
779
+ <w:t>${categoryCounter[category]++}${options.inFootnote ? "A" : ""}</w:t>
780
+ </w:r>
781
+ <w:r>
782
+ <w:rPr></w:rPr>
783
+ <w:fldChar w:fldCharType="end" />
784
+ </w:r>`
785
+ if (caption.length) {
786
+ caption = [{type: "text", text: ": "}].concat(caption)
787
+ }
788
+ }
789
+ let cx, cy
790
+ const image =
791
+ node.content.find(node => node.type === "image")?.attrs
792
+ .image || false
793
+ if (image !== false) {
794
+ const imageEntry = this.images.images[image]
795
+ cx = imageEntry.width * 9525 // width in EMU
796
+ cy = imageEntry.height * 9525 // height in EMU
797
+ const imgTitle = imageEntry.title
798
+ // Shrink image if too large for paper.
799
+ if (options.dimensions) {
800
+ let width = options.dimensions.width
801
+ if (options.tableSideMargins) {
802
+ width = width - options.tableSideMargins
803
+ }
804
+ width =
805
+ (width * Number.parseInt(node.attrs.width)) / 100
806
+ if (cx > width) {
807
+ const rel = cy / cx
808
+ cx = width
809
+ cy = cx * rel
810
+ }
811
+ if (cy > options.dimensions.height) {
812
+ const rel = cx / cy
813
+ cy = options.dimensions.height
814
+ cx = cy * rel
815
+ }
816
+ }
817
+ cy = Math.round(cy)
818
+ cx = Math.round(cx)
819
+ const rId = imageEntry.id
820
+ content += `<w:r>
821
+ <w:rPr></w:rPr>
822
+ <w:drawing>
823
+ <wp:inline distT="0" distB="0" distL="0" distR="0">
824
+ <wp:extent cx="${cx}" cy="${cy}"/>
825
+ <wp:docPr id="${++this.docPrCount}" name="Picture${this.docPrCount}" descr=""/>
826
+ <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
827
+ <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
828
+ <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
829
+ <pic:nvPicPr>
830
+ <pic:cNvPr id="0" name="${imgTitle}" descr=""/>
831
+ <pic:cNvPicPr>
832
+ <a:picLocks noChangeAspect="1" noChangeArrowheads="1"/>
833
+ </pic:cNvPicPr>
834
+ </pic:nvPicPr>
835
+ <pic:blipFill>
836
+ <a:blip r:embed="rId${rId}"/>
837
+ <a:stretch>
838
+ <a:fillRect/>
839
+ </a:stretch>
840
+ </pic:blipFill>
841
+ <pic:spPr bwMode="auto">
842
+ <a:xfrm>
843
+ <a:off x="0" y="0"/>
844
+ <a:ext cx="${cx}" cy="${cy}"/>
845
+ </a:xfrm>
846
+ <a:prstGeom prst="rect">
847
+ <a:avLst/>
848
+ </a:prstGeom>
849
+ <a:noFill/>
850
+ <a:ln w="9525">
851
+ <a:noFill/>
852
+ <a:miter lim="800000"/>
853
+ <a:headEnd/>
854
+ <a:tailEnd/>
855
+ </a:ln>
856
+ </pic:spPr>
857
+ </pic:pic>
858
+ </a:graphicData>
859
+ </a:graphic>
860
+ </wp:inline>
861
+ </w:drawing>
862
+ </w:r>`
863
+ } else {
864
+ cx = 9525 * 100 // We pick a random size of 100x100. We hope this will fit the formula
865
+ cy = 9525 * 100
866
+ const latex =
867
+ node.content.find(
868
+ node => node.type === "figure_equation"
869
+ )?.attrs.equation || ""
870
+ content += this.math.getOmml(latex)
871
+ }
872
+ const captionSpace = !!(catCountXML.length || caption.length)
873
+ if (node.attrs.aligned === "center") {
874
+ start += `
875
+ <w:p>
876
+ <w:pPr>
877
+ <w:jc w:val="center"/>
878
+ </w:pPr>`
879
+ content =
880
+ `<w:bookmarkStart w:name="${node.attrs.id}" w:id="${++this.bookmarkCounter}"/><w:bookmarkEnd w:id="${this.bookmarkCounter}"/>` +
881
+ content
882
+ end =
883
+ `
884
+ </w:p>
885
+ ${
886
+ captionSpace
887
+ ? `<w:p>
888
+ <w:pPr><w:pStyle w:val="Caption"/><w:rPr></w:rPr></w:pPr>
889
+ ${catCountXML}
890
+ ${caption.map((node, i) => this.transformRichtext(node, options, caption[i + 1])).join("")}
891
+ </w:p>`
892
+ : ""
893
+ }` + end
894
+ } else {
895
+ start += `
896
+ <w:p>
897
+ <w:pPr>
898
+ <w:jc w:val="center"/>
899
+ </w:pPr>
900
+ <w:r>
901
+ <w:rPr></w:rPr>
902
+ <w:drawing>
903
+ <wp:anchor behindDoc="0" distT="95250" distB="95250" distL="95250" distR="95250" simplePos="0" locked="0" layoutInCell="1" allowOverlap="0" relativeHeight="2">
904
+ <wp:simplePos x="0" y="0" />
905
+ <wp:positionH relativeFrom="column">
906
+ <wp:align>${node.attrs.aligned}</wp:align>
907
+ </wp:positionH>
908
+ <wp:positionV relativeFrom="paragraph">
909
+ <wp:posOffset>0</wp:posOffset>
910
+ </wp:positionV>
911
+ <wp:extent cx="${cx}" cy="${captionSpace ? cy + 350520 : cy}" />
912
+ <wp:effectExtent l="0" t="0" r="0" b="0" />
913
+ <wp:wrapSquare wrapText="largest" />
914
+ <wp:docPr id="${++this.docPrCount}" name="Frame${this.docPrCount}" />
915
+ <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
916
+ <a:graphicData uri="http://schemas.microsoft.com/office/word/2010/wordprocessingShape">
917
+ <wps:wsp>
918
+ <wps:cNvSpPr txBox="1" />
919
+ <wps:spPr>
920
+ <a:xfrm>
921
+ <a:off x="0" y="0" />
922
+ <a:ext cx="${cx}" cy="${captionSpace ? cy + 350520 : cy}" />
923
+ </a:xfrm>
924
+ <a:prstGeom prst="rect" />
925
+ </wps:spPr>
926
+ <wps:txbx>
927
+ <w:txbxContent>
928
+ <w:p>
929
+ <w:pPr>
930
+ <w:pStyle w:val="Caption" />
931
+ <w:spacing w:before="20" w:after="220" />
932
+ <w:rPr></w:rPr>
933
+ </w:pPr>`
934
+ content =
935
+ `<w:bookmarkStart w:name="${node.attrs.id}" w:id="${++this.bookmarkCounter}"/><w:bookmarkEnd w:id="${this.bookmarkCounter}"/>` +
936
+ content
937
+ end =
938
+ `
939
+ ${catCountXML}
940
+ ${caption.map((node, i) => this.transformRichtext(node, options, caption[i + 1])).join("")}
941
+ </w:p>
942
+ </w:txbxContent>
943
+ </wps:txbx>
944
+ <wps:bodyPr anchor="t" lIns="0" tIns="0" rIns="0" bIns="0">
945
+ <a:noAutofit />
946
+ </wps:bodyPr>
947
+ </wps:wsp>
948
+ </a:graphicData>
949
+ </a:graphic>
950
+ <wp14:sizeRelH relativeFrom="margin">
951
+ <wp14:pctWidth>${node.attrs.width}000</wp14:pctWidth>
952
+ </wp14:sizeRelH>
953
+ </wp:anchor>
954
+ </w:drawing>
955
+ </w:r>
956
+ </w:p>` + end
957
+ }
958
+ if (blockInsert) {
959
+ start += `<w:ins w:id="${++this.changeCounter}" w:author="${escapeText(blockInsert.username)}" w:date="${new Date(blockInsert.date * 60000).toISOString().split(".")[0]}Z">`
960
+ end = "</w:ins>" + end
961
+ }
962
+ if (blockDelete) {
963
+ start += `<w:del w:id="${++this.changeCounter}" w:author="${escapeText(blockDelete.username)}" w:date="${new Date(blockDelete.date * 60000).toISOString().split(".")[0]}Z">`
964
+ end = "</w:del>" + end
965
+ }
966
+ break
967
+ }
968
+ case "figure_caption":
969
+ // We are already dealing with this in the figure. Prevent content from being added a second time.
970
+ return ""
971
+ case "figure_equation":
972
+ // We are already dealing with this in the figure.
973
+ break
974
+ case "image":
975
+ // We are already dealing with this in the figure.
976
+ break
977
+ case "table": {
978
+ const category = node.attrs.category
979
+ let caption = node.attrs.caption
980
+ ? node.content[0].content || []
981
+ : []
982
+ let catCountXML = ""
983
+ if (category !== "none") {
984
+ const categoryCounter = options.inFootnote
985
+ ? this.fncategoryCounter
986
+ : this.categoryCounter
987
+ if (!categoryCounter[category]) {
988
+ categoryCounter[category] = 1
989
+ }
990
+ catCountXML = `<w:r>
991
+ <w:t xml:space="preserve">${CATS[category][this.settings.language]} </w:t>
992
+ </w:r>
993
+ <w:r>
994
+ <w:rPr></w:rPr>
995
+ <w:fldChar w:fldCharType="begin"></w:fldChar>
996
+ </w:r>
997
+ <w:r>
998
+ <w:rPr></w:rPr>
999
+ <w:instrText> SEQ ${category} \\* ARABIC </w:instrText>
1000
+ </w:r>
1001
+ <w:r>
1002
+ <w:rPr></w:rPr>
1003
+ <w:fldChar w:fldCharType="separate" />
1004
+ </w:r>
1005
+ <w:r>
1006
+ <w:rPr></w:rPr>
1007
+ <w:t>${categoryCounter[category]++}${options.inFootnote ? "A" : ""}</w:t>
1008
+ </w:r>
1009
+ <w:r>
1010
+ <w:rPr></w:rPr>
1011
+ <w:fldChar w:fldCharType="end" />
1012
+ </w:r>`
1013
+ if (caption.length) {
1014
+ caption = [{type: "text", text: ": "}].concat(caption)
1015
+ }
1016
+ }
1017
+ const captionSpace = !!(catCountXML.length || caption.length)
1018
+ if (captionSpace) {
1019
+ start += `
1020
+ <w:p>
1021
+ <w:pPr>
1022
+ <w:pStyle w:val="Caption"/>
1023
+ <w:keepNext/>
1024
+ </w:pPr>
1025
+ <w:bookmarkStart w:name="${node.attrs.id}" w:id="${++this.bookmarkCounter}"/>
1026
+ <w:bookmarkEnd w:id="${this.bookmarkCounter}"/>
1027
+ ${catCountXML}
1028
+ ${caption.map((node, i) => this.transformRichtext(node, options, caption[i + 1])).join("")}
1029
+ </w:p>`
1030
+ }
1031
+ this.tables.addTableGridStyle()
1032
+ start += `
1033
+ <w:tbl>
1034
+ <w:tblPr>
1035
+ <w:tblStyle w:val="${this.tables.tableGridStyle}" />
1036
+ ${
1037
+ node.attrs.width === "100"
1038
+ ? '<w:tblW w:w="0" w:type="auto" />'
1039
+ : `<w:tblW w:w="${50 * Number.parseInt(node.attrs.width)}" w:type="pct" />
1040
+ <w:jc w:val="${node.attrs.aligned}" />`
1041
+ }
1042
+ <w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1" />
1043
+ </w:tblPr>
1044
+ <w:tblGrid>`
1045
+ const columns = node.content[1].content[0].content.length
1046
+ let cellWidth = 63500 // standard width
1047
+ options = Object.assign({}, options)
1048
+ if (options.dimensions?.width) {
1049
+ cellWidth =
1050
+ Number.parseInt(options.dimensions.width / columns) -
1051
+ 2540 // subtracting for border width
1052
+ } else if (!options.dimensions) {
1053
+ options.dimensions = {}
1054
+ }
1055
+ options.section = "Normal"
1056
+ options.list_type = null
1057
+ options.dimensions = Object.assign({}, options.dimensions)
1058
+ options.dimensions.width = cellWidth
1059
+ options.tableSideMargins = this.tables.getSideMargins()
1060
+ for (let i = 0; i < columns; i++) {
1061
+ start += `<w:gridCol w:w="${Number.parseInt(cellWidth / 635)}" />`
1062
+ }
1063
+ start += "</w:tblGrid>"
1064
+ end = "</w:tbl>" + end
1065
+
1066
+ break
1067
+ }
1068
+ case "table_body":
1069
+ // Pass through to table.
1070
+ break
1071
+ case "table_caption":
1072
+ // We already deal with this in 'table'.
1073
+ return ""
1074
+ case "table_row":
1075
+ start += "<w:tr>"
1076
+ end = "</w:tr>" + end
1077
+ break
1078
+ case "table_cell":
1079
+ case "table_header":
1080
+ start += `
1081
+ <w:tc>
1082
+ <w:tcPr>
1083
+ ${
1084
+ node.attrs.rowspan && node.attrs.colspan
1085
+ ? `<w:tcW w:w="${Number.parseInt((options.dimensions?.width || 0) / 635)}" w:type="dxa" />`
1086
+ : '<w:tcW w:w="0" w:type="auto" />'
1087
+ }
1088
+ ${
1089
+ node.attrs.rowspan
1090
+ ? node.attrs.rowspan > 1
1091
+ ? '<w:vMerge w:val="restart" />'
1092
+ : ""
1093
+ : "<w:vMerge/>"
1094
+ }
1095
+ ${
1096
+ node.attrs.colspan
1097
+ ? node.attrs.colspan > 1
1098
+ ? '<w:hMerge w:val="restart" />'
1099
+ : ""
1100
+ : "<w:hMerge/>"
1101
+ }
1102
+ </w:tcPr>
1103
+ ${node.content ? "" : "<w:p/>"}`
1104
+ end = "</w:tc>" + end
1105
+
1106
+ break
1107
+ case "equation": {
1108
+ const latex = node.attrs.equation
1109
+ content += this.math.getOmml(latex)
1110
+ break
1111
+ }
1112
+ case "hard_break":
1113
+ content += "<w:r><w:br/></w:r>"
1114
+ break
1115
+ // CSL bib entries
1116
+ case "cslbib":
1117
+ options = Object.assign({}, options)
1118
+ options.section = "Bibliography"
1119
+ break
1120
+ case "cslblock":
1121
+ end = "<w:r><w:br/></w:r>" + end
1122
+ break
1123
+ case "cslleftmargin":
1124
+ end = "<w:r><w:tab/></w:r>" + end
1125
+ break
1126
+ case "cslindent":
1127
+ start += "<w:r><w:tab/></w:r>"
1128
+ end = "<w:r><w:br/></w:r>" + end
1129
+ break
1130
+ case "cslentry":
1131
+ start += `
1132
+ <w:p>
1133
+ <w:pPr>
1134
+ <w:pStyle w:val="${options.section}"/>
1135
+ <w:rPr></w:rPr>
1136
+ </w:pPr>`
1137
+ // Note - beginning is in same par as first item, whereas end is in its own par
1138
+ if (node.attrs?.first) {
1139
+ start += `<w:r>
1140
+ <w:fldChar w:fldCharType="begin"/>
1141
+ </w:r>
1142
+ <w:r>
1143
+ <w:instrText xml:space="preserve"> ADDIN ZOTERO_BIBL CSL_BIBLIOGRAPHY </w:instrText>
1144
+ </w:r>
1145
+ <w:r>
1146
+ <w:fldChar w:fldCharType="separate"/>
1147
+ </w:r>`
1148
+ }
1149
+ end = "</w:p>" + end
1150
+ if (node.attrs?.last) {
1151
+ end =
1152
+ end +
1153
+ `<w:p>
1154
+ <w:pPr>
1155
+ <w:rPr/>
1156
+ </w:pPr>
1157
+ <w:r>
1158
+ <w:fldChar w:fldCharType="end"/>
1159
+ </w:r>
1160
+ </w:p>`
1161
+ }
1162
+ break
1163
+ case "cslinline":
1164
+ case "cslrightinline":
1165
+ break
1166
+ default:
1167
+ console.warn("Unknown node type:", node.type, node)
1168
+ break
1169
+ }
1170
+
1171
+ if (node.content) {
1172
+ for (let i = 0; i < node.content.length; i++) {
1173
+ content += this.transformRichtext(
1174
+ node.content[i],
1175
+ options,
1176
+ node.content[i + 1]
1177
+ )
1178
+ }
1179
+ }
1180
+ return start + content + end
1181
+ }
1182
+ }