@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,260 @@
1
+ import {descendantNodes} from "../tools/doc_content.js"
2
+
3
+ const DEFAULT_LISTPARAGRAPH_XML = `
4
+ <w:style w:type="paragraph" w:styleId="ListParagraph">
5
+ <w:name w:val="List Paragraph"/>
6
+ <w:basedOn w:val="Normal"/>
7
+ <w:uiPriority w:val="34"/>
8
+ <w:qFormat/>
9
+ <w:rsid w:val="006E68A6"/>
10
+ <w:pPr>
11
+ <w:ind w:left="720"/>
12
+ <w:contextualSpacing/>
13
+ </w:pPr>
14
+ </w:style>
15
+ `
16
+
17
+ const DEFAULT_NUMBERING_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
18
+ <w:numbering xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se wp14">
19
+ </w:numbering>`
20
+
21
+ export class DOCXExporterLists {
22
+ constructor(docContent, xml, rels) {
23
+ this.docContent = docContent
24
+ this.xml = xml
25
+ this.rels = rels
26
+ this.useBulletList = false
27
+ this.usedNumberedList = []
28
+ this.styleXML = false
29
+ this.numberingXML = false
30
+ this.abstractNumIdCounter = 0
31
+ this.numIdCounter = 0
32
+ // We only need one bulletType for all bullet lists, but a new
33
+ // numberedType for each numbered list so that the numbering starts in 1
34
+ // each time.
35
+ this.bulletType = false
36
+ this.numberFormat = "decimal"
37
+ this.numberedTypes = []
38
+ this.styleFilePath = "word/styles.xml"
39
+ this.numberingFilePath = "word/numbering.xml"
40
+ this.ctFilePath = "[Content_Types].xml"
41
+ }
42
+
43
+ init() {
44
+ this.findLists()
45
+ if (this.usedNumberedList.length > 0 || this.useBulletList) {
46
+ const p = []
47
+
48
+ p.push(
49
+ new Promise(resolve => {
50
+ this.initCt().then(() => resolve())
51
+ })
52
+ )
53
+
54
+ p.push(
55
+ new Promise(resolve => {
56
+ this.addNumberingXml().then(() => resolve())
57
+ })
58
+ )
59
+
60
+ p.push(
61
+ new Promise(resolve => {
62
+ this.addListParagraphStyle().then(() => resolve())
63
+ })
64
+ )
65
+ return Promise.all(p)
66
+ } else {
67
+ return Promise.resolve()
68
+ }
69
+ }
70
+
71
+ initCt() {
72
+ return this.xml.getXml(this.ctFilePath).then(ctXML => {
73
+ this.ctXML = ctXML
74
+ this.addRelsToCt()
75
+ return Promise.resolve()
76
+ })
77
+ }
78
+
79
+ addRelsToCt() {
80
+ const override = this.ctXML.query("Override", {
81
+ PartName: `/${this.numberingFilePath}`
82
+ })
83
+ if (!override) {
84
+ const types = this.ctXML.query("Types")
85
+ types.appendXML(
86
+ `<Override PartName="/${this.numberingFilePath}" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>`
87
+ )
88
+ }
89
+ }
90
+
91
+ findLists() {
92
+ descendantNodes(this.docContent).forEach(node => {
93
+ if (node.type === "bullet_list") {
94
+ this.useBulletList = true
95
+ } else if (node.type === "ordered_list") {
96
+ this.usedNumberedList.push(node.attrs.order)
97
+ }
98
+ })
99
+ }
100
+
101
+ addNumberingXml() {
102
+ return this.xml
103
+ .getXml(this.numberingFilePath, DEFAULT_NUMBERING_XML)
104
+ .then(numberingXML => {
105
+ this.numberingXML = numberingXML
106
+ this.rels.addNumberingRel()
107
+ this.addUsedListTypes()
108
+ return Promise.resolve()
109
+ })
110
+ }
111
+
112
+ addListParagraphStyle() {
113
+ return this.xml.getXml(this.styleFilePath).then(styleXML => {
114
+ this.styleXML = styleXML
115
+ if (
116
+ !this.styleXML.query("w:style", {"w:styleId": "ListParagraph"})
117
+ ) {
118
+ const stylesEl = this.styleXML.query("w:styles")
119
+ stylesEl.appendXML(DEFAULT_LISTPARAGRAPH_XML)
120
+ }
121
+ return Promise.resolve()
122
+ })
123
+ }
124
+
125
+ addUsedListTypes() {
126
+ const allAbstractNum = this.numberingXML.queryAll("w:abstractNum")
127
+ allAbstractNum.forEach(abstractNum => {
128
+ // We check the format for the lowest level list and use the first
129
+ // one we find for 'bullet' or 'not bullet'
130
+ // This means that if a list is defined using anything else than
131
+ // bullets, it will be accepted as the format of
132
+ // the numeric list.
133
+ const levelZeroFormat = abstractNum
134
+ .query("w:lvl", {"w:ilvl": "0"})
135
+ .query("w:numFmt")
136
+ .getAttribute("w:val")
137
+ const abstractNumId = Number.parseInt(
138
+ abstractNum.getAttribute("w:abstractNumId")
139
+ )
140
+ if (levelZeroFormat === "bullet" && !this.bulletAbstractType) {
141
+ const numEl = this.numberingXML.query("w:abstractNumId", {
142
+ "w:val": abstractNumId
143
+ }).parentElement
144
+ const numId = Number.parseInt(numEl.getAttribute("w:numId"))
145
+ this.bulletType = numId
146
+ } else if (levelZeroFormat !== "bullet" && !this.numberFormat) {
147
+ this.numberFormat = levelZeroFormat
148
+ }
149
+ if (this.abstractNumIdCounter < abstractNumId) {
150
+ this.abstractNumIdCounter = abstractNumId
151
+ }
152
+ })
153
+ const allNum = this.numberingXML.queryAll("w:num")
154
+ allNum.forEach(numEl => {
155
+ const numId = Number.parseInt(numEl.getAttribute("w:val"))
156
+ if (this.numIdCounter < numId) {
157
+ this.numIdCounter = numId
158
+ }
159
+ })
160
+
161
+ if (!this.bulletType && this.useBulletList) {
162
+ this.addBulletNumType(
163
+ ++this.numIdCounter,
164
+ ++this.abstractNumIdCounter
165
+ )
166
+ this.bulletType = this.numIdCounter
167
+ }
168
+ if (this.usedNumberedList.length > 0) {
169
+ this.abstractNumIdCounter++
170
+
171
+ this.numberedAbstractType = this.abstractNumIdCounter
172
+ }
173
+ for (let i = 0; i < this.usedNumberedList.length; i++) {
174
+ const numId = ++this.numIdCounter
175
+ this.addNumberedNumType(numId, this.usedNumberedList[i])
176
+ this.numberedTypes.push(numId)
177
+ }
178
+ }
179
+
180
+ getBulletType() {
181
+ return this.bulletType
182
+ }
183
+
184
+ getNumberedType() {
185
+ return this.numberedTypes.shift()
186
+ }
187
+
188
+ addBulletNumType(numId, abstractNumId) {
189
+ const numberingEl = this.numberingXML.query("w:numbering")
190
+ numberingEl.appendXML(`
191
+ <w:abstractNum w:abstractNumId="${abstractNumId}" w15:restartNumberingAfterBreak="0">
192
+ <w:nsid w:val="3620195A" />
193
+ <w:multiLevelType w:val="hybridMultilevel" />
194
+ <w:tmpl w:val="A74C9E6A" />
195
+ </w:abstractNum>
196
+ <w:num w:numId="${numId}">
197
+ <w:abstractNumId w:val="${abstractNumId}" />
198
+ </w:num>
199
+ `)
200
+ const newAbstractNum = this.numberingXML.query("w:abstractNum", {
201
+ "w:abstractNumId": String(abstractNumId)
202
+ })
203
+ // Definition seem to always define 9 levels (0-8).
204
+ for (let level = 0; level < 9; level++) {
205
+ newAbstractNum.appendXML(`
206
+ <w:lvl w:ilvl="${level}" w:tplc="04090001" w:tentative="1">
207
+ <w:start w:val="1" />
208
+ <w:numFmt w:val="bullet" />
209
+ <w:lvlText w:val="•" />
210
+ <w:lvlJc w:val="left" />
211
+ <w:pPr>
212
+ <w:ind w:left="${(level + 1) * 720}" w:hanging="360" />
213
+ </w:pPr>
214
+ <w:rPr>
215
+ <w:rFonts w:ascii="Symbol" w:hAnsi="Symbol" />
216
+ </w:rPr>
217
+ </w:lvl>
218
+ `)
219
+ }
220
+ }
221
+
222
+ addNumberedNumType(numId, start) {
223
+ this.abstractNumIdCounter++
224
+ this.addNumberedAbstractNumType(this.abstractNumIdCounter, start)
225
+ const numberingEl = this.numberingXML.query("w:numbering")
226
+ numberingEl.appendXML(`
227
+ <w:num w:numId="${numId}">
228
+ <w:abstractNumId w:val="${this.abstractNumIdCounter}" />
229
+ </w:num>
230
+ `)
231
+ }
232
+
233
+ addNumberedAbstractNumType(abstractNumId, start) {
234
+ const numberingEl = this.numberingXML.query("w:numbering")
235
+ numberingEl.appendXML(`
236
+ <w:abstractNum w:abstractNumId="${abstractNumId}" w15:restartNumberingAfterBreak="0">
237
+ <w:nsid w:val="7F6635F3" />
238
+ <w:multiLevelType w:val="hybridMultilevel" />
239
+ <w:tmpl w:val="BFFEF214" />
240
+ </w:abstractNum>
241
+ `)
242
+ const newAbstractNum = this.numberingXML.query("w:abstractNum", {
243
+ "w:abstractNumId": String(abstractNumId)
244
+ })
245
+ // Definition seem to always define 9 levels (0-8).
246
+ for (let level = 0; level < 9; level++) {
247
+ newAbstractNum.appendXML(`
248
+ <w:lvl w:ilvl="${level}" w:tplc="0409000F">
249
+ <w:start w:val="${start}" />
250
+ <w:numFmt w:val="${this.numberFormat}" />
251
+ <w:lvlText w:val="%${level + 1}." />
252
+ <w:lvlJc w:val="left" />
253
+ <w:pPr>
254
+ <w:ind w:left="${(level + 1) * 720}" w:hanging="360" />
255
+ </w:pPr>
256
+ </w:lvl>
257
+ `)
258
+ }
259
+ }
260
+ }
@@ -0,0 +1,46 @@
1
+ import {mml2omml} from "mathml2omml"
2
+
3
+ // Not entirely sure if we need this font here. This is included whenever Word
4
+ // itself adds a formula, but our ooml doesn't refer to the font, so it may be pointless.
5
+ const CAMBRIA_MATH_FONT_DECLARATION = `
6
+ <w:font w:name="Cambria Math">
7
+ <w:panose1 w:val="02040503050406030204" />
8
+ <w:charset w:val="00" />
9
+ <w:family w:val="roman" />
10
+ <w:pitch w:val="variable" />
11
+ <w:sig w:usb0="E00002FF" w:usb1="420024FF" w:usb2="00000000" w:usb3="00000000" w:csb0="0000019F" w:csb1="00000000" />
12
+ </w:font>`
13
+
14
+ export class DOCXExporterMath {
15
+ constructor(xml) {
16
+ this.xml = xml
17
+ this.fontTableXML = false
18
+ this.addedCambriaMath = false
19
+ this.domParser = new DOMParser()
20
+ }
21
+
22
+ init() {
23
+ return this.xml
24
+ .getXml("word/fontTable.xml")
25
+ .then(fontTablesXML => {
26
+ this.fontTablesXML = fontTablesXML
27
+ return import("mathlive")
28
+ })
29
+ .then(MathLive => (this.mathLive = MathLive))
30
+ }
31
+
32
+ latexToMathML(latex) {
33
+ return this.mathLive.convertLatexToMathMl(latex)
34
+ }
35
+
36
+ getOmml(latex) {
37
+ if (!this.addedCambriaMath) {
38
+ const fontsEl = this.fontTablesXML.query("w:fonts")
39
+ fontsEl.appendXML(CAMBRIA_MATH_FONT_DECLARATION)
40
+ this.addedCambriaMath = true
41
+ }
42
+ const mathmlString = `<math xmlns="http://www.w3.org/1998/Math/MathML"><semantics>${this.latexToMathML(latex)}</semantics></math>`
43
+ const ommlString = mml2omml(mathmlString)
44
+ return ommlString
45
+ }
46
+ }
@@ -0,0 +1,289 @@
1
+ import {escapeText} from "../../common/index.js"
2
+
3
+ export class DOCXExporterMetadata {
4
+ constructor(xml, metadata, csl = null) {
5
+ this.xml = xml
6
+ this.metadata = metadata
7
+ this.csl = csl
8
+ this.coreXML = false
9
+ this.customXML = false
10
+ }
11
+
12
+ init() {
13
+ return this.xml.getXml("docProps/core.xml").then(coreXML => {
14
+ this.coreXML = coreXML
15
+ this.addMetadata()
16
+ return this.addCustomProperties()
17
+ })
18
+ }
19
+
20
+ async hasBibliography() {
21
+ if (!this.csl || !this.metadata.citationStyle) {
22
+ return "0"
23
+ }
24
+ try {
25
+ const style = await this.csl.getStyle(this.metadata.citationStyle)
26
+ // Check if the style has a bibliography section
27
+ const hasBib = style.children.some(
28
+ section => section.name === "bibliography"
29
+ )
30
+ return hasBib ? "1" : "0"
31
+ } catch (_error) {
32
+ return "0"
33
+ }
34
+ }
35
+
36
+ addMetadata() {
37
+ const corePropertiesEl = this.coreXML.query("cp:coreProperties")
38
+
39
+ // Title
40
+ let titleEl = this.coreXML.query("dc:title")
41
+ if (!titleEl) {
42
+ corePropertiesEl.appendXML("<dc:title></dc:title>")
43
+ titleEl = corePropertiesEl.lastElementChild
44
+ }
45
+ titleEl.innerXML = escapeText(this.metadata.title)
46
+ // Authors
47
+
48
+ const authors = this.metadata.authors.map(author => {
49
+ const nameParts = []
50
+ if (author.firstname) {
51
+ nameParts.push(author.firstname)
52
+ }
53
+ if (author.lastname) {
54
+ nameParts.push(author.lastname)
55
+ }
56
+ if (!nameParts.length && author.institution) {
57
+ // We have an institution but no names. Use institution as name.
58
+ nameParts.push(author.institution)
59
+ }
60
+ return nameParts.join(" ")
61
+ })
62
+ const lastAuthor = authors.length
63
+ ? escapeText(authors[0])
64
+ : gettext("Unknown")
65
+ const allAuthors = authors.length
66
+ ? escapeText(authors.join(";"))
67
+ : gettext("Unknown")
68
+ let allAuthorsEl = this.coreXML.query("dc:creator")
69
+
70
+ if (!allAuthorsEl) {
71
+ corePropertiesEl.appendXML("<dc:creator></dc:creator>")
72
+ allAuthorsEl = corePropertiesEl.lastElementChild
73
+ }
74
+ allAuthorsEl.innerXML = allAuthors
75
+ let lastAuthorEl = this.coreXML.query("dc:lastModifiedBy")
76
+ if (!lastAuthorEl) {
77
+ corePropertiesEl.appendXML(
78
+ "<dc:lastModifiedBy></dc:lastModifiedBy>"
79
+ )
80
+ lastAuthorEl = corePropertiesEl.lastElementChild
81
+ }
82
+ lastAuthorEl.innerXML = lastAuthor
83
+ // Keywords
84
+ if (this.metadata.keywords.length) {
85
+ // It is not really clear how keywords should be separated in DOCX files,
86
+ // so we use ", ".
87
+ const keywordsString = escapeText(this.metadata.keywords.join(", "))
88
+
89
+ let keywordsEl = this.coreXML.query("cp:keywords")
90
+ if (!keywordsEl) {
91
+ corePropertiesEl.appendXML("<cp:keywords></cp:keywords>")
92
+ keywordsEl = corePropertiesEl.lastElementChild
93
+ }
94
+ keywordsEl.innerXML = keywordsString
95
+ }
96
+
97
+ // time
98
+ const date = new Date()
99
+ const dateString = date.toISOString().split(".")[0] + "Z"
100
+ const createdEl = this.coreXML.query("dcterms:created")
101
+ createdEl.innerXML = dateString
102
+ let modifiedEl = this.coreXML.query("dcterms:modified")
103
+ if (!modifiedEl) {
104
+ corePropertiesEl.appendXML(
105
+ '<dcterms:modified xsi:type="dcterms:W3CDTF"></dcterms:modified>'
106
+ )
107
+ modifiedEl = corePropertiesEl.lastElementChild
108
+ }
109
+ modifiedEl.innerXML = dateString
110
+ }
111
+
112
+ async addCustomProperties() {
113
+ // Create or update docProps/custom.xml with citation style information
114
+ const customXmlContent = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
115
+ <Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
116
+ </Properties>`
117
+
118
+ const customXML = await this.xml.getXml(
119
+ "docProps/custom.xml",
120
+ Promise.resolve(customXmlContent)
121
+ )
122
+ this.customXML = customXML
123
+
124
+ // Add citation style property
125
+ if (this.metadata.citationStyle) {
126
+ const propertiesEl = this.customXML.query("Properties")
127
+
128
+ // Remove any existing ZOTERO_PREF_ properties
129
+ const existingZoteroProps = this.customXML
130
+ .queryAll("property")
131
+ .filter(
132
+ prop =>
133
+ prop.getAttribute("name") &&
134
+ prop.getAttribute("name").startsWith("ZOTERO_PREF_")
135
+ )
136
+ existingZoteroProps.forEach(prop =>
137
+ prop.parentElement.removeChild(prop)
138
+ )
139
+
140
+ // Find the highest pid to determine the next one
141
+ const existingProperties = this.customXML.queryAll("property")
142
+ let maxPid = 0
143
+ existingProperties.forEach(prop => {
144
+ const pid = parseInt(prop.getAttribute("pid"))
145
+ if (pid > maxPid) {
146
+ maxPid = pid
147
+ }
148
+ })
149
+
150
+ // Determine if the citation style has a bibliography
151
+ const hasBib = await this.hasBibliography()
152
+
153
+ // Create the data content
154
+ const citationStyleUrl = `http://www.zotero.org/styles/${escapeText(this.metadata.citationStyle)}`
155
+ const dataContent = `<data data-version="3" zotero-version="8.0.2"><session id=""/><style id="${citationStyleUrl}" locale="${escapeText(this.metadata.language || "en-US")}" hasBibliography="${hasBib}" bibliographyStyleHasBeenSet="1"/><prefs><pref name="fieldType" value="Field"/></prefs></data>`
156
+
157
+ // Split content into chunks of 255 characters (DOCX limit)
158
+ const chunkSize = 255
159
+ const chunks = []
160
+ for (let i = 0; i < dataContent.length; i += chunkSize) {
161
+ chunks.push(dataContent.substring(i, i + chunkSize))
162
+ }
163
+
164
+ // Create properties for each chunk
165
+ chunks.forEach((chunk, index) => {
166
+ const propName = `ZOTERO_PREF_${index + 1}`
167
+ const propertyXML = `<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${maxPid + index + 1}" name="${propName}">
168
+ <vt:lpwstr></vt:lpwstr>
169
+ </property>`
170
+ propertiesEl.appendXML(propertyXML)
171
+ // Set the text content after appending (textContent escapes XML characters)
172
+ const lpwstrEl =
173
+ propertiesEl.lastElementChild.query("vt:lpwstr")
174
+ if (lpwstrEl) {
175
+ lpwstrEl.textContent = chunk
176
+ }
177
+ })
178
+ }
179
+
180
+ // Add structured contributor metadata properties
181
+ await this.addContributorProperties()
182
+
183
+ return Promise.resolve()
184
+ }
185
+
186
+ async addContributorProperties() {
187
+ if (!this.metadata.contributors || !this.metadata.contributors.length) {
188
+ return Promise.resolve()
189
+ }
190
+
191
+ const propertiesEl = this.customXML.query("Properties")
192
+
193
+ // Remove any existing fidus_contributor_ properties
194
+ const existingContributorProps = this.customXML
195
+ .queryAll("property")
196
+ .filter(
197
+ prop =>
198
+ prop.getAttribute("name") &&
199
+ prop.getAttribute("name").startsWith("fidus_contributor_")
200
+ )
201
+ existingContributorProps.forEach(prop =>
202
+ prop.parentElement.removeChild(prop)
203
+ )
204
+
205
+ // Find the highest pid
206
+ const existingProperties = this.customXML.queryAll("property")
207
+ let maxPid = 0
208
+ existingProperties.forEach(prop => {
209
+ const pid = parseInt(prop.getAttribute("pid"))
210
+ if (pid > maxPid) {
211
+ maxPid = pid
212
+ }
213
+ })
214
+
215
+ const contributors = this.metadata.contributors
216
+
217
+ // Add contributor count
218
+ maxPid++
219
+ const countXML = `<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${maxPid}" name="fidus_contributor_count">
220
+ <vt:i4></vt:i4>
221
+ </property>`
222
+ propertiesEl.appendXML(countXML)
223
+ const countEl = propertiesEl.lastElementChild.query("vt:i4")
224
+ if (countEl) {
225
+ countEl.textContent = String(contributors.length)
226
+ }
227
+
228
+ // Add property for each contributor
229
+ contributors.forEach((contributor, index) => {
230
+ const num = index + 1
231
+ const nameParts = []
232
+ if (contributor.firstname) {
233
+ nameParts.push(contributor.firstname)
234
+ }
235
+ if (contributor.lastname) {
236
+ nameParts.push(contributor.lastname)
237
+ }
238
+ const fullName =
239
+ nameParts.join(" ") || contributor.institution || ""
240
+
241
+ const fields = [
242
+ {
243
+ name: `fidus_contributor_${num}_role`,
244
+ value: contributor.role || ""
245
+ },
246
+ {name: `fidus_contributor_${num}_name`, value: fullName},
247
+ {
248
+ name: `fidus_contributor_${num}_firstname`,
249
+ value: contributor.firstname || ""
250
+ },
251
+ {
252
+ name: `fidus_contributor_${num}_lastname`,
253
+ value: contributor.lastname || ""
254
+ },
255
+ {
256
+ name: `fidus_contributor_${num}_institution`,
257
+ value: contributor.institution || ""
258
+ },
259
+ {
260
+ name: `fidus_contributor_${num}_email`,
261
+ value: contributor.email || ""
262
+ },
263
+ {
264
+ name: `fidus_contributor_${num}_id_type`,
265
+ value: contributor.id_type || ""
266
+ },
267
+ {
268
+ name: `fidus_contributor_${num}_id_value`,
269
+ value: contributor.id_value || ""
270
+ }
271
+ ]
272
+
273
+ fields.forEach(field => {
274
+ maxPid++
275
+ const propertyXML = `<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" pid="${maxPid}" name="${field.name}">
276
+ <vt:lpwstr></vt:lpwstr>
277
+ </property>`
278
+ propertiesEl.appendXML(propertyXML)
279
+ const lpwstrEl =
280
+ propertiesEl.lastElementChild.query("vt:lpwstr")
281
+ if (lpwstrEl) {
282
+ lpwstrEl.textContent = field.value
283
+ }
284
+ })
285
+ })
286
+
287
+ return Promise.resolve()
288
+ }
289
+ }