@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,865 @@
1
+ import {escapeText} from "../../common/index.js"
2
+ import {CATS} from "../../schema/i18n.js"
3
+ import {createZoteroCitation} from "../tools/zotero_csl.js"
4
+
5
+ const TEXT_TYPES = {
6
+ heading1: {tag: "text:h", attrs: _options => 'text:outline-level="1"'},
7
+ heading2: {tag: "text:h", attrs: _options => 'text:outline-level="2"'},
8
+ heading3: {tag: "text:h", attrs: _options => 'text:outline-level="3"'},
9
+ heading4: {tag: "text:h", attrs: _options => 'text:outline-level="4"'},
10
+ heading5: {tag: "text:h", attrs: _options => 'text:outline-level="5"'},
11
+ heading6: {tag: "text:h", attrs: _options => 'text:outline-level="6"'},
12
+ paragraph: {
13
+ tag: "text:p",
14
+ attrs: options => `text:style-name="${options.section}"`
15
+ },
16
+ code_block: {
17
+ tag: "text:p",
18
+ attrs: _options => 'text:style-name="Preformatted_20_Text"'
19
+ }
20
+ }
21
+
22
+ const INLINE_TYPES = [
23
+ "citation",
24
+ "cross_reference",
25
+ "cslbib",
26
+ "cslblock",
27
+ "cslindent",
28
+ "cslinline",
29
+ "cslleftmargin",
30
+ "cslrightinline",
31
+ "equation",
32
+ "footnote",
33
+ "hard_break",
34
+ "image",
35
+ "text"
36
+ ]
37
+
38
+ /**
39
+ * Create Zotero reference mark name for ODT.
40
+ * @param {Array} references - Array of {id, prefix?, locator?} from citation node
41
+ * @param {Object} bibDB - Bibliography database
42
+ * @param {string} formattedCitation - Pre-formatted citation text from citeproc
43
+ * @param {string} citationId - Optional citation ID (generated if not provided)
44
+ * @returns {string} Reference mark name with JSON encoded
45
+ */
46
+
47
+ // Generate a random ID for Zotero bibliography section + Zotero citations
48
+ // Format: RND + random alphanumeric string (10 characters)
49
+ function generateZoteroId() {
50
+ const chars =
51
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
52
+ const length = 10
53
+ let result = "RND"
54
+ for (let i = 0; i < length; i++) {
55
+ result += chars.charAt(Math.floor(Math.random() * chars.length))
56
+ }
57
+ return result
58
+ }
59
+
60
+ function createZoteroCitationMark(
61
+ references,
62
+ bibDB,
63
+ formattedCitation,
64
+ citationId = null
65
+ ) {
66
+ const zoteroCitation = createZoteroCitation(
67
+ references,
68
+ bibDB,
69
+ formattedCitation,
70
+ citationId
71
+ )
72
+ if (!zoteroCitation) {
73
+ return null
74
+ }
75
+
76
+ const jsonStr = JSON.stringify(zoteroCitation)
77
+ // In ODT, quotes must be encoded as &quot; in attribute values
78
+ const encodedJson = jsonStr.replace(/"/g, "&quot;")
79
+ const zoteroId = generateZoteroId()
80
+ return `ZOTERO_ITEM CSL_CITATION ${encodedJson} ${zoteroId}`
81
+ }
82
+
83
+ export class ODTExporterRichtext {
84
+ constructor(
85
+ comments,
86
+ settings,
87
+ styles,
88
+ tracks,
89
+ footnotes,
90
+ citations,
91
+ math,
92
+ images
93
+ ) {
94
+ this.comments = comments
95
+ this.styles = styles
96
+ this.tracks = tracks
97
+ this.footnotes = footnotes
98
+ this.citations = citations
99
+ this.settings = settings
100
+ this.math = math
101
+ this.images = images
102
+
103
+ this.imgCounter = 1
104
+ this.fnCounter = 0 // real footnotes
105
+ this.fnAlikeCounter = 0 // real footnotes and citations as footnotes
106
+ this.categoryCounter = {} // counters for each type of table/figure category (figure/table/photo)
107
+ this.fnCategoryCounter = {} // counters for each type of table/figure category (figure/table/photo)
108
+ this.zIndex = 0
109
+ this.citationCounter = 0 // Track which citation we're processing
110
+ }
111
+
112
+ run(node, options = {}, parent = null, siblingIndex = 0) {
113
+ options.comments = this.findComments(node) // Data related to comments. We need to mark the first and last occurence of comment
114
+ return this.transformRichtext(node, options, parent, siblingIndex)
115
+ }
116
+
117
+ findComments(node, comments = {}) {
118
+ if (node.marks) {
119
+ node.marks
120
+ .filter(mark => mark.type === "comment")
121
+ .forEach(comment => {
122
+ if (!comments[comment.attrs.id]) {
123
+ comments[comment.attrs.id] = {
124
+ start: node,
125
+ end: node,
126
+ content: this.comments[comment.attrs.id]
127
+ }
128
+ } else {
129
+ comments[comment.attrs.id]["end"] = node
130
+ }
131
+ })
132
+ }
133
+ if (node.content) {
134
+ for (let i = 0; i < node.content.length; i++) {
135
+ this.findComments(node.content[i], comments)
136
+ }
137
+ }
138
+ return comments
139
+ }
140
+
141
+ transformRichtext(node, options = {}, parent = null, siblingIndex = 0) {
142
+ let start = "",
143
+ content = "",
144
+ end = ""
145
+ const siblings = parent?.content || []
146
+ const previousSibling = siblings[siblingIndex - 1]
147
+ const nextSibling = siblings[siblingIndex + 1]
148
+
149
+ const inlineNode = INLINE_TYPES.includes(node.type)
150
+
151
+ let blockDelete, blockInsert
152
+
153
+ if (!inlineNode && node.attrs?.track) {
154
+ blockDelete = node.attrs.track.find(
155
+ mark => mark.type === "deletion"
156
+ )
157
+ if (blockDelete) {
158
+ options = Object.assign({}, options)
159
+ options.blockDelete = blockDelete
160
+ }
161
+ blockInsert = node.attrs.track.find(
162
+ mark => mark.type === "insertion"
163
+ )
164
+ if (blockInsert) {
165
+ options = Object.assign({}, options)
166
+ options.blockInsert = blockInsert
167
+ }
168
+ }
169
+
170
+ if (node.marks) {
171
+ node.marks
172
+ .filter(mark => mark.type === "comment")
173
+ .forEach(comment => {
174
+ const commentData = options.comments[comment.attrs.id]
175
+ if (!commentData || !commentData.content) {
176
+ return
177
+ }
178
+ if (commentData.start === node) {
179
+ start += `<office:annotation office:name="comment_${options.tag}_${comment.attrs.id}" loext:resolved="${commentData.content.resolved}">
180
+ <dc:creator>${escapeText(commentData.content.username)}</dc:creator>
181
+ <dc:date>${new Date(commentData.content.date).toISOString().slice(0, -1)}000000</dc:date>
182
+ ${commentData.content.comment.map(node => this.transformRichtext(node, options)).join("")}
183
+ </office:annotation>`
184
+ }
185
+ if (commentData.end === node) {
186
+ end =
187
+ `<office:annotation-end office:name="comment_${options.tag}_${comment.attrs.id}"/>` +
188
+ (commentData.content.answers || [])
189
+ .map(
190
+ answer =>
191
+ `<office:annotation loext:resolved="${commentData.content.resolved}">
192
+ <dc:creator>${escapeText(answer.username)}</dc:creator>
193
+ <dc:date>${new Date(answer.date).toISOString().slice(0, -1)}000000</dc:date>
194
+ ${answer.answer.map(node => this.transformRichtext(node, options)).join("")}
195
+ </office:annotation>`
196
+ )
197
+ .join("") +
198
+ end
199
+ }
200
+ })
201
+ }
202
+
203
+ switch (node.type) {
204
+ case "bibliography_heading":
205
+ this.styles.checkParStyle("Bibliography_20_Heading")
206
+ start += '<text:p text:style-name="Bibliography_20_Heading">'
207
+ end = "</text:p>" + end
208
+ break
209
+ case "code_block":
210
+ case "heading1":
211
+ case "heading2":
212
+ case "heading3":
213
+ case "heading4":
214
+ case "heading5":
215
+ case "heading6":
216
+ case "paragraph": {
217
+ // Handles all types of text blocks.
218
+ if (node.type === "code_block") {
219
+ this.styles.checkParStyle("Preformatted_20_Text")
220
+ } else if (node.type === "paragraph") {
221
+ if (!options.section) {
222
+ options.section = "Text_20_body"
223
+ }
224
+ this.styles.checkParStyle(options.section)
225
+ }
226
+ const nextBlockDelete = nextSibling?.attrs?.track?.find(
227
+ mark => mark.type === "deletion"
228
+ )
229
+ const nextBlockInsert = nextSibling?.attrs?.track?.find(
230
+ mark => mark.type === "insertion"
231
+ )
232
+ let lastNonMergedBlock
233
+ if (blockDelete) {
234
+ // This block has been deleted, so we need to check which text block
235
+ // it is being merged in to. If it has, we need to merge the
236
+ // two blocks.
237
+ if (!previousSibling || !TEXT_TYPES[previousSibling.type]) {
238
+ // We cannot merge into previous block. Therefore, we don't consider
239
+ // this text block as merged.
240
+ blockDelete = false
241
+ } else {
242
+ let searchNode = previousSibling
243
+ while (searchNode && TEXT_TYPES[searchNode.type]) {
244
+ lastNonMergedBlock = searchNode
245
+ if (
246
+ searchNode?.attrs?.track?.find(
247
+ mark => mark.type === "deletion"
248
+ )
249
+ ) {
250
+ searchNode =
251
+ siblings[siblings.indexOf(searchNode) - 1]
252
+ } else {
253
+ searchNode = false
254
+ }
255
+ }
256
+ }
257
+ }
258
+ if (blockDelete) {
259
+ // This block has been deleted, so instead we just add a text
260
+ // change marker.
261
+ if (previousSibling.type === "paragraph") {
262
+ if (!options.section) {
263
+ options.section = "Text_20_body"
264
+ }
265
+ this.styles.checkParStyle(options.section)
266
+ }
267
+ const trackId = this.tracks.addChange(
268
+ blockDelete,
269
+ `
270
+ <${TEXT_TYPES[previousSibling.type].tag} ${TEXT_TYPES[previousSibling.type].attrs(options)}/>
271
+ <${TEXT_TYPES[node.type].tag} ${TEXT_TYPES[node.type].attrs(options)}/>`
272
+ )
273
+ start += `<text:change text:change-id="${trackId}"/>`
274
+ } else {
275
+ start += `<${TEXT_TYPES[node.type].tag} ${TEXT_TYPES[node.type].attrs(options)}>`
276
+ }
277
+ if (blockInsert && blockInsert.trackId) {
278
+ // The previous block node is a text node , so the insertion is a textblock split.
279
+ // We need to put change track marks in both this and the previous text block.
280
+ start += `<text:change-end text:change-id="${blockInsert.trackId}"/>`
281
+ }
282
+ const nextBlockDeleteTextType =
283
+ nextBlockDelete && TEXT_TYPES[nextSibling.type]
284
+ if (!nextBlockDeleteTextType) {
285
+ const lastNonMergedBlockTextType =
286
+ lastNonMergedBlock &&
287
+ TEXT_TYPES[lastNonMergedBlock.type]
288
+ if (lastNonMergedBlockTextType) {
289
+ // This block has been deleted and the next block is not.
290
+ // So we end it here as the last known non-deleted block type.
291
+ end = `</${lastNonMergedBlockTextType.tag}>` + end
292
+ } else {
293
+ // The next block is not deleted, so we close this block.
294
+ end = `</${TEXT_TYPES[node.type].tag}>` + end
295
+ }
296
+ }
297
+ if (nextBlockInsert && TEXT_TYPES[nextSibling.type]) {
298
+ // The following block node is a text node , so the insertion is a textblock split.
299
+ // We need to put change track marks in both this and the next text block.
300
+ const trackId = this.tracks.addChange(nextBlockInsert)
301
+ end =
302
+ `<text:change-start text:change-id="${trackId}"/>` + end
303
+ // Adding the track id here so that we can add find it at the beginning of the next text block.
304
+ nextBlockInsert.trackId = trackId
305
+ }
306
+ if (TEXT_TYPES[node.type].tag === "text:h") {
307
+ start += `<text:bookmark-start text:name="${node.attrs.id}"/>`
308
+ end =
309
+ `<text:bookmark-end text:name="${node.attrs.id}"/>` +
310
+ end
311
+ }
312
+ // Handle code block category labels
313
+ if (node.type === "code_block") {
314
+ const category = node.attrs.category
315
+ if (category && node.attrs.id) {
316
+ const categoryCounter = options.inFootnote
317
+ ? this.fnCategoryCounter
318
+ : this.categoryCounter
319
+ if (!categoryCounter[category]) {
320
+ categoryCounter[category] = 1
321
+ }
322
+ const catCount = categoryCounter[category]++
323
+ const catCountXml = `<text:sequence text:ref-name="ref${category}${catCount - 1}${options.inFootnote ? "A" : ""}" text:name="${category}" text:formula="ooow:${category}+1" style:num-format="1">${catCount}${options.inFootnote ? "A" : ""}</text:sequence>`
324
+ const title = node.attrs.title
325
+ ? `: ${escapeText(node.attrs.title)}`
326
+ : ""
327
+ const categoryLabel = `<text:bookmark-start text:name="${node.attrs.id}"/>${CATS[category][this.settings.language]} ${catCountXml}${title}<text:bookmark-end text:name="${node.attrs.id}"/><text:line-break/>`
328
+ start += categoryLabel
329
+ }
330
+ }
331
+ break
332
+ }
333
+ case "blockquote":
334
+ // This is imperfect, but Word doesn't seem to provide section/quotation nesting
335
+ options = Object.assign({}, options)
336
+ options.section = "Quote"
337
+ break
338
+ case "ordered_list": {
339
+ const olId = this.styles.getOrderedListStyleId(node.attrs.order)
340
+ start += `<text:list text:style-name="L${olId[0]}">`
341
+ end = "</text:list>" + end
342
+ options = Object.assign({}, options)
343
+ options.section = `P${olId[1]}`
344
+ options.listStyles = (options.listStyles || []).concat([
345
+ `L${olId[0]}`
346
+ ])
347
+ break
348
+ }
349
+ case "bullet_list": {
350
+ const ulId = this.styles.getBulletListStyleId()
351
+ start += `<text:list text:style-name="L${ulId[0]}">`
352
+ end = "</text:list>" + end
353
+ options = Object.assign({}, options)
354
+ options.section = `P${ulId[1]}`
355
+ options.listStyles = (options.listStyles || []).concat([
356
+ `L${ulId[0]}`
357
+ ])
358
+ break
359
+ }
360
+ case "list_item":
361
+ start += "<text:list-item>"
362
+ end = "</text:list-item>" + end
363
+ break
364
+ case "footnotecontainer":
365
+ break
366
+ case "footnote": {
367
+ const fnCounter = this.fnAlikeCounter++
368
+ const fnOptions = Object.assign({}, options)
369
+ fnOptions.section = "Footnote"
370
+ fnOptions.tag = `footnote${fnCounter}`
371
+ fnOptions.inFootnote = true
372
+ const fnNode = {
373
+ type: "footnotecontainer",
374
+ content: node.attrs.footnote
375
+ }
376
+ fnOptions.comments = this.findComments(fnNode)
377
+ content += this.transformRichtext(fnNode, fnOptions)
378
+ start += `
379
+ <text:note text:id="ftn${fnCounter}" text:note-class="footnote">
380
+ <text:note-citation>${fnCounter}</text:note-citation>
381
+ <text:note-body>`
382
+ end =
383
+ `
384
+ </text:note-body>
385
+ </text:note>` + end
386
+ break
387
+ }
388
+ case "text": {
389
+ let hyperlink,
390
+ strong,
391
+ em,
392
+ underline,
393
+ sup,
394
+ sub,
395
+ smallcaps,
396
+ code,
397
+ anchor
398
+ // Check for hyperlink, bold/strong and italic/em
399
+ if (node.marks) {
400
+ hyperlink = node.marks.find(mark => mark.type === "link")
401
+ anchor = node.marks.find(mark => mark.type === "anchor")
402
+ strong = node.marks.find(mark => mark.type === "strong")
403
+ em = node.marks.find(mark => mark.type === "em")
404
+ underline = node.marks.find(
405
+ mark => mark.type === "underline"
406
+ )
407
+ smallcaps = node.marks.find(
408
+ mark => mark.type === "smallcaps"
409
+ )
410
+ sup = node.marks.find(mark => mark.type === "sup")
411
+ sub = node.marks.find(mark => mark.type === "sub")
412
+ code = node.marks.find(mark => mark.type === "code")
413
+ }
414
+
415
+ if (hyperlink) {
416
+ start += `<text:a xlink:type="simple" xlink:href="${escapeText(hyperlink.attrs.href)}">`
417
+ end = "</text:a>" + end
418
+ }
419
+ if (anchor) {
420
+ start += `<text:reference-mark-start text:name="${anchor.attrs.id}"/>`
421
+ end =
422
+ `<text:reference-mark-end text:name="${anchor.attrs.id}"/>` +
423
+ end
424
+
425
+ start += `<text:bookmark-start text:name="${anchor.attrs.id}"/>`
426
+ end =
427
+ `<text:bookmark-end text:name="${anchor.attrs.id}"/>` +
428
+ end
429
+ }
430
+
431
+ let attributes = ""
432
+
433
+ if (em) {
434
+ attributes += "e"
435
+ }
436
+ if (strong) {
437
+ attributes += "s"
438
+ }
439
+ if (underline) {
440
+ attributes += "u"
441
+ }
442
+ if (smallcaps) {
443
+ attributes += "c"
444
+ }
445
+ if (sup) {
446
+ attributes += "p"
447
+ } else if (sub) {
448
+ attributes += "b"
449
+ }
450
+ if (code) {
451
+ attributes += "t"
452
+ }
453
+
454
+ if (attributes.length) {
455
+ const styleId = this.styles.getInlineStyleId(attributes)
456
+ start += `<text:span text:style-name="T${styleId}">`
457
+ end = "</text:span>" + end
458
+ }
459
+
460
+ content += escapeText(node.text).replace(/^\s+|\s+$/g, match =>
461
+ "<text:s/>".repeat(match.length)
462
+ )
463
+ break
464
+ }
465
+ case "citation": {
466
+ let cit
467
+ // We take the first citation from the stack and remove it.
468
+ if (options.inFootnote) {
469
+ cit = this.footnotes.citations.pmCits.shift()
470
+ } else {
471
+ cit = this.citations.pmCits.shift()
472
+ }
473
+
474
+ // Get citation info and formatted text for Zotero export
475
+ const citInfo = this.citations.citInfos[this.citationCounter]
476
+ const formattedText =
477
+ this.citations.citationTexts[this.citationCounter]
478
+ this.citationCounter++
479
+
480
+ // Create Zotero citation data on-the-fly
481
+ const markName =
482
+ citInfo && formattedText
483
+ ? createZoteroCitationMark(
484
+ citInfo.references,
485
+ this.citations.bibDB,
486
+ formattedText
487
+ )
488
+ : null
489
+
490
+ if (options.citationType === "note" && !options.inFootnote) {
491
+ // If the citations are in notes (footnotes), we need to
492
+ // put the contents of this citation in a footnote.
493
+
494
+ if (markName && formattedText) {
495
+ // Create Zotero reference mark for footnote citation
496
+ start += `
497
+ <text:note text:id="ftn${this.fnAlikeCounter++}" text:note-class="footnote">
498
+ <text:note-citation>${this.fnAlikeCounter}</text:note-citation>
499
+ <text:note-body>
500
+ <text:p text:style-name="Footnote">
501
+ <text:reference-mark-start text:name="${markName}"/>`
502
+ content = formattedText
503
+ end =
504
+ `<text:reference-mark-end text:name="${markName}"/></text:p>
505
+ </text:note-body>
506
+ </text:note>` + end
507
+ } else {
508
+ // Fallback to non-Zotero format
509
+ start += `
510
+ <text:note text:id="ftn${this.fnAlikeCounter++}" text:note-class="footnote">
511
+ <text:note-citation>${this.fnAlikeCounter}</text:note-citation>
512
+ <text:note-body>`
513
+ end =
514
+ `
515
+ </text:note-body>
516
+ </text:note>` + end
517
+ options = Object.assign({}, options)
518
+ options.section = "Footnote"
519
+ content += this.transformRichtext(
520
+ {type: "paragraph", content: cit.content},
521
+ options
522
+ )
523
+ }
524
+ } else {
525
+ // For in-text citations, create Zotero reference mark
526
+ if (markName && formattedText) {
527
+ start += `<text:reference-mark-start text:name="${markName}"/>`
528
+ content = formattedText
529
+ end =
530
+ `<text:reference-mark-end text:name="${markName}"/>` +
531
+ end
532
+ } else {
533
+ // Fallback to formatted text only
534
+ cit.content.forEach(citContent => {
535
+ content += this.transformRichtext(
536
+ citContent,
537
+ options
538
+ )
539
+ })
540
+ }
541
+ }
542
+
543
+ break
544
+ }
545
+ case "figure": {
546
+ // NOTE: The difficulty is to make several images with different
547
+ // alignments/widths not overlap one-another. The below code
548
+ // makes a reasonable attempt at that, but it seems there is no
549
+ // way to guarantee it from happening.
550
+ options = Object.assign({}, options)
551
+ options.section = "Standard"
552
+ this.styles.checkParStyle(options.section)
553
+ start += `<text:p text:style-name="${options.section}">`
554
+ end = "</text:p>" + end
555
+
556
+ if (node.attrs.aligned === "center") {
557
+ // Needed to prevent subsequent image from overlapping
558
+ end = end + '<text:p text:style-name="Standard"></text:p>'
559
+ }
560
+ const figureCaption = node.content.find(
561
+ node => node.type === "figure_caption"
562
+ )
563
+ let caption = node.attrs.caption
564
+ ? figureCaption?.content
565
+ ?.map((node, index) =>
566
+ this.transformRichtext(
567
+ node,
568
+ options,
569
+ figureCaption,
570
+ index
571
+ )
572
+ )
573
+ .join("") || ""
574
+ : ""
575
+ // The figure category should not be in the
576
+ // user's language but rather the document language
577
+ const category = node.attrs.category
578
+ if (category !== "none") {
579
+ const categoryCounter = options.inFootnote
580
+ ? this.fnCategoryCounter
581
+ : this.categoryCounter
582
+ if (!categoryCounter[category]) {
583
+ categoryCounter[category] = 1
584
+ }
585
+ const catCount = categoryCounter[category]++
586
+ const catCountXml = `<text:sequence text:ref-name="ref${category}${catCount - 1}${options.inFootnote ? "A" : ""}" text:name="${category}" text:formula="ooow:${category}+1" style:num-format="1">${catCount}${options.inFootnote ? "A" : ""}</text:sequence>`
587
+ if (caption.length) {
588
+ caption = `<text:bookmark-start text:name="${node.attrs.id}"/>${CATS[category][this.settings.language]} ${catCountXml}<text:bookmark-end text:name="${node.attrs.id}"/>: ${caption}`
589
+ } else {
590
+ caption = `<text:bookmark-start text:name="${node.attrs.id}"/>${CATS[category][this.settings.language]} ${catCountXml}<text:bookmark-end text:name="${node.attrs.id}"/>`
591
+ }
592
+ }
593
+ let relWidth = node.attrs.width
594
+ let aligned = node.attrs.aligned
595
+ let frame
596
+ const image =
597
+ node.content.find(node => node.type === "image")?.attrs
598
+ .image || false
599
+ if (caption.length || image === false) {
600
+ frame = true
601
+ this.styles.checkParStyle("Caption")
602
+ this.styles.checkParStyle("Figure")
603
+ const graphicStyleId = this.styles.getGraphicStyleId(
604
+ "Frame",
605
+ aligned
606
+ )
607
+ start += `<draw:frame draw:style-name="fr${graphicStyleId}" draw:name="Frame${graphicStyleId}" text:anchor-type="paragraph" svg:width="0.0161in" style:rel-width="${relWidth}%" draw:z-index="${this.zIndex++}">
608
+ <draw:text-box fo:min-height="0in">
609
+ <text:p text:style-name="Figure">`
610
+ relWidth = "100" // percentage width of image inside of frame is always 100
611
+ aligned = "center" // Aligned inside of frame is always 'center'
612
+ end =
613
+ `
614
+ </text:p>
615
+ </draw:text-box>
616
+ </draw:frame>` + end
617
+ if (caption.length) {
618
+ end = `<text:line-break />${caption}` + end
619
+ }
620
+ }
621
+ if (image !== false) {
622
+ const imageEntry = this.images.images[image]
623
+
624
+ const height = (imageEntry.height * 3) / 4 // more or less px to point
625
+ const width = (imageEntry.width * 3) / 4 // more or less px to point
626
+ const graphicStyleId = this.styles.getGraphicStyleId(
627
+ "Graphics",
628
+ aligned
629
+ )
630
+ content += `
631
+ <draw:frame draw:style-name="${graphicStyleId}" draw:name="Image${this.imgCounter++}" text:anchor-type="${frame && !blockInsert ? "char" : "as-char"}" style:rel-width="${relWidth}%" style:rel-height="scale" svg:width="${width}pt" svg:height="${height}pt" draw:z-index="${this.zIndex++}">
632
+ ${
633
+ imageEntry.svg
634
+ ? `<draw:image xlink:href="Pictures/${imageEntry.svg}" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" draw:mime-type="image/svg+xml"/>`
635
+ : ""
636
+ }
637
+ <draw:image xlink:href="Pictures/${imageEntry.id}" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad" draw:mime-type="${imageEntry.type}"/>
638
+ </draw:frame>`
639
+ } else {
640
+ const latex = node.content.find(
641
+ node => node.type === "figure_equation"
642
+ )?.attrs.equation
643
+ const objectNumber = this.math.addMath(latex)
644
+ const graphicStyleId =
645
+ this.styles.getGraphicStyleId("Formula")
646
+ content += `
647
+ <draw:frame draw:style-name="${graphicStyleId}" draw:name="Object${objectNumber}" text:anchor-type="as-char" draw:z-index="${this.zIndex++}">
648
+ <draw:object xlink:href="./Object ${objectNumber}.js" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>
649
+ <svg:desc>formula</svg:desc>
650
+ </draw:frame>`
651
+ }
652
+ if (category === "none") {
653
+ content = `<text:bookmark-start text:name="${node.attrs.id}"/>${content}<text:bookmark-end text:name="${node.attrs.id}"/>`
654
+ }
655
+ if (blockDelete) {
656
+ const trackId = this.tracks.addChange(
657
+ blockDelete,
658
+ `<text:p text:style-name="Figure">${content}<text:span>‍‍</text:span></text:p>`
659
+ )
660
+ content = `<text:change text:change-id="${trackId}"/>`
661
+ }
662
+ if (blockInsert) {
663
+ const trackId = this.tracks.addChange(blockInsert)
664
+ start += `<text:change-start text:change-id="${trackId}"/>`
665
+ end = `<text:change-end text:change-id="${trackId}"/>` + end
666
+ }
667
+ break
668
+ }
669
+ case "figure_caption":
670
+ // We are already dealing with this in the figure. Prevent content from being added a second time.
671
+ return ""
672
+ case "figure_equation":
673
+ // We are already dealing with this in the figure.
674
+ break
675
+ case "image":
676
+ // We are already dealing with this in the figure.
677
+ break
678
+ case "table": {
679
+ if (options.listStyles) {
680
+ options.listStyles.forEach(listStyle => {
681
+ end =
682
+ `<text:list text:continue-numbering="true" text:style-name="${listStyle}"><text:list-item>` +
683
+ end
684
+ start += "</text:list-item></text:list>"
685
+ })
686
+ }
687
+ const tableCaption = node.content[0]
688
+ let caption = node.attrs.caption
689
+ ? tableCaption?.content
690
+ ?.map((node, index) =>
691
+ this.transformRichtext(
692
+ node,
693
+ options,
694
+ tableCaption,
695
+ index
696
+ )
697
+ )
698
+ .join("") || ""
699
+ : ""
700
+ // The table category should not be in the
701
+ // user's language but rather the document language
702
+ const category = node.attrs.category
703
+ if (category !== "none") {
704
+ const categoryCounter = options.inFootnote
705
+ ? this.fnCategoryCounter
706
+ : this.categoryCounter
707
+ if (!categoryCounter[category]) {
708
+ categoryCounter[category] = 1
709
+ }
710
+ const catCount = categoryCounter[category]++
711
+ const catCountXml = `<text:sequence text:ref-name="ref${category}${catCount - 1}${options.inFootnote ? "A" : ""}" text:name="${category}" text:formula="ooow:${category}+1" style:num-format="1">${catCount}${options.inFootnote ? "A" : ""}</text:sequence>`
712
+ if (caption.length) {
713
+ caption = `<text:bookmark-start text:name="${node.attrs.id}"/>${CATS[category][this.settings.language]} ${catCountXml}<text:bookmark-end text:name="${node.attrs.id}"/>: ${caption}`
714
+ } else {
715
+ caption = `<text:bookmark-start text:name="${node.attrs.id}"/>${CATS[category][this.settings.language]} ${catCountXml}<text:bookmark-end text:name="${node.attrs.id}"/>`
716
+ }
717
+ }
718
+ if (caption.length) {
719
+ if (!options.section) {
720
+ options.section = "Text_20_body"
721
+ }
722
+ this.styles.checkParStyle(options.section)
723
+ start += `<text:p text:style-name="${options.section}">${caption}</text:p>`
724
+ }
725
+ const columns = node.content[1].content[0].content.length
726
+ const styleId = this.styles.getTableStyleId(
727
+ node.attrs.aligned,
728
+ node.attrs.width
729
+ )
730
+ start += `<table:table table:name="Table${styleId}" table:style-name="Table${styleId}">`
731
+ start += `<table:table-column table:number-columns-repeated="${columns}" />`
732
+ end = "</table:table>" + end
733
+ break
734
+ }
735
+ case "table_body":
736
+ // Pass through to table.
737
+ break
738
+ case "table_caption":
739
+ // We already deal with this in 'table'.
740
+ return ""
741
+ case "table_row":
742
+ start += "<table:table-row>"
743
+ end = "</table:table-row>" + end
744
+ break
745
+ case "table_cell":
746
+ case "table_header":
747
+ if (node.attrs.rowspan && node.attrs.colspan) {
748
+ start += `<table:table-cell${
749
+ node.attrs.rowspan > 1
750
+ ? ` table:number-rows-spanned="${node.attrs.rowspan}"`
751
+ : ""
752
+ }${
753
+ node.attrs.colspan > 1
754
+ ? ` table:number-columns-spanned="${node.attrs.colspan}"`
755
+ : ""
756
+ } office:value-type="string">`
757
+ end = "</table:table-cell>" + end
758
+ } else {
759
+ start += "<table:covered-table-cell/>"
760
+ }
761
+ break
762
+ case "equation": {
763
+ const latex = node.attrs.equation
764
+ const objectNumber = this.math.addMath(latex)
765
+ const styleId = this.styles.getGraphicStyleId("Formula")
766
+ content += `<draw:frame draw:style-name="${styleId}" draw:name="Object${objectNumber}" text:anchor-type="as-char" draw:z-index="${this.zIndex++}">
767
+ <draw:object xlink:href="./Object ${objectNumber}.js" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/>
768
+ <svg:desc>formula</svg:desc>
769
+ </draw:frame>`
770
+ break
771
+ }
772
+ case "cross_reference": {
773
+ const title = node.attrs.title
774
+ const id = node.attrs.id
775
+ if (title) {
776
+ start += `<text:bookmark-ref text:reference-format="text" text:ref-name="${id}">`
777
+ end = "</text:bookmark-ref>" + end
778
+ }
779
+ content += escapeText(title || "MISSING TARGET")
780
+ break
781
+ }
782
+ case "hard_break":
783
+ content += "<text:line-break/>"
784
+ break
785
+ // CSL bib entries
786
+ case "cslbib": {
787
+ options = Object.assign({}, options)
788
+ options.section = "Bibliography_20_1"
789
+ // Ensure Sect1 section style exists
790
+ this.styles.checkSectionStyle("Sect1")
791
+ // Generate a unique bibliography ID for this document
792
+ const bibId = generateZoteroId()
793
+ start += `<text:section text:style-name="Sect1" text:name="ZOTERO_BIBL CSL_BIBLIOGRAPHY ${bibId}">`
794
+ end = "</text:section>" + end
795
+ break
796
+ }
797
+ case "cslblock":
798
+ end = "<text:line-break/>" + end
799
+ break
800
+ case "cslleftmargin":
801
+ end = "<text:tab/>" + end
802
+ break
803
+ case "cslindent":
804
+ start += "<text:tab/>"
805
+ end = "<text:line-break/>" + end
806
+ break
807
+ case "cslentry":
808
+ this.styles.checkParStyle(options.section)
809
+ start += `<text:p text:style-name="${options.section}">`
810
+ end = "</text:p>" + end
811
+ break
812
+ case "cslinline":
813
+ case "cslrightinline":
814
+ break
815
+ default:
816
+ break
817
+ }
818
+
819
+ if (node.content) {
820
+ for (let i = 0; i < node.content.length; i++) {
821
+ content += this.transformRichtext(
822
+ node.content[i],
823
+ options,
824
+ node,
825
+ i
826
+ )
827
+ }
828
+ }
829
+
830
+ if (inlineNode) {
831
+ const inlineInsert =
832
+ node.marks?.find(
833
+ mark =>
834
+ mark.type === "insertion" &&
835
+ mark.attrs.approved === false
836
+ )?.attrs || blockInsert
837
+ const inlineDelete =
838
+ node.marks?.find(mark => mark.type === "deletion")?.attrs ||
839
+ options.blockDelete
840
+ if (inlineDelete) {
841
+ if (parent) {
842
+ const trackId = this.tracks.addChange(
843
+ Object.assign({type: "deletion"}, inlineDelete),
844
+ `<${TEXT_TYPES[parent.type]?.tag || "text:p"} ${TEXT_TYPES[parent.type]?.attrs(options) || `text:style-name="${options.section}"`}>${
845
+ start + content + end
846
+ }</${TEXT_TYPES[parent.type]?.tag || "text:p"}>`
847
+ )
848
+ content = `<text:change text:change-id="${trackId}"/>`
849
+ } else {
850
+ content = ""
851
+ }
852
+ start = ""
853
+ end = ""
854
+ }
855
+ if (inlineInsert) {
856
+ const trackId = this.tracks.addChange(
857
+ Object.assign({type: "insertion"}, inlineInsert)
858
+ )
859
+ start += `<text:change-start text:change-id="${trackId}"/>`
860
+ end = `<text:change-end text:change-id="${trackId}"/>` + end
861
+ }
862
+ }
863
+ return start + content + end
864
+ }
865
+ }