@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,165 @@
1
+ import {escapeText} from "../../common/index.js"
2
+ import {descendantNodes} from "../tools/doc_content.js"
3
+
4
+ const DEFAULT_COMMENTS_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
5
+ <w:comments xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" mc:Ignorable="w14 wp14 w15">
6
+ </w:comments>`
7
+
8
+ const DEFAULT_COMMENTS_EXTENDED_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9
+ <w15:commentsEx xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" mc:Ignorable="w15">
10
+ </w15:commentsEx>`
11
+
12
+ export class DOCXExporterComments {
13
+ constructor(docContent, commentsDB, xml, rels, richtext) {
14
+ this.docContent = docContent
15
+ this.commentsDB = commentsDB
16
+ this.xml = xml
17
+ this.rels = rels
18
+ this.richtext = richtext
19
+
20
+ this.usedComments = []
21
+ this.commentsXML = false
22
+ this.commentsExtendedXML = false
23
+ this.commentsFilePath = "word/comments.xml"
24
+ this.commentsExtendedFilePath = "word/commentsExtended.xml"
25
+ this.commentIdCounter = -1
26
+ }
27
+
28
+ init() {
29
+ let useExtended = false
30
+ descendantNodes(this.docContent).forEach(node => {
31
+ if (node.marks) {
32
+ const comments = node.marks.filter(
33
+ mark => mark.type === "comment"
34
+ )
35
+ comments.forEach(comment => {
36
+ if (
37
+ !this.usedComments.includes(comment.attrs.id) &&
38
+ this.commentsDB[comment.attrs.id]
39
+ ) {
40
+ this.usedComments.push(comment.attrs.id)
41
+ if (
42
+ this.commentsDB[comment.attrs.id].resolved ||
43
+ this.commentsDB[comment.attrs.id].answers?.length
44
+ ) {
45
+ useExtended = true
46
+ }
47
+ }
48
+ })
49
+ }
50
+ })
51
+ if (!this.usedComments.length) {
52
+ return Promise.resolve()
53
+ }
54
+ this.rels.addCommentsRel()
55
+ const addCommentXMLs = [
56
+ this.xml
57
+ .getXml(this.commentsFilePath, DEFAULT_COMMENTS_XML)
58
+ .then(commentsXML => (this.commentsXML = commentsXML))
59
+ ]
60
+ if (useExtended) {
61
+ this.rels.addCommentsExtendedRel()
62
+ addCommentXMLs.push(
63
+ this.xml
64
+ .getXml(
65
+ this.commentsExtendedFilePath,
66
+ DEFAULT_COMMENTS_EXTENDED_XML
67
+ )
68
+ .then(
69
+ commentsExtendedXML =>
70
+ (this.commentsExtendedXML = commentsExtendedXML)
71
+ )
72
+ )
73
+ }
74
+ return Promise.all(addCommentXMLs).then(() => {
75
+ this.commentsXML.queryAll("w:comment").forEach(el => {
76
+ const id = Number.parseInt(el.getAttribute("w:id"))
77
+ if (id > this.commentIdCounter) {
78
+ this.commentIdCounter = id
79
+ }
80
+ })
81
+ return this.exportComments()
82
+ })
83
+ }
84
+
85
+ addComment(id) {
86
+ const commentId = ++this.commentIdCounter
87
+ this.richtext.comments[id] = commentId
88
+ const commentDBEntry = this.commentsDB[id]
89
+ const comments = this.commentsXML.query("w:comments")
90
+ let string = `<w:comment w:id="${commentId}" w:author="${escapeText(commentDBEntry.username)}" w:date="${new Date(commentDBEntry.date).toISOString().split(".")[0]}Z" w:initials="${escapeText(
91
+ commentDBEntry.username
92
+ .split(" ")
93
+ .map(n => n[0])
94
+ .join("")
95
+ .toUpperCase()
96
+ )}">`
97
+ let parentParagraphId = ""
98
+ string += commentDBEntry.comment
99
+ .map((node, index) => {
100
+ const options = {section: "CommentText"}
101
+ if (
102
+ (commentDBEntry.resolved ||
103
+ commentDBEntry.answers?.length) &&
104
+ index === commentDBEntry.comment.length - 1
105
+ ) {
106
+ // If comment has been resolved or there are answers, we need to add an id to the last paragraph
107
+ // of the comment and add an entry into commentsExtended.xml.
108
+ parentParagraphId = (++this.richtext.paragraphIdCounter)
109
+ .toString(16)
110
+ .padStart(8, "0")
111
+ options.paragraphId = parentParagraphId
112
+ const extendedString = `<w15:commentEx w15:paraId="${parentParagraphId}" w15:done="${commentDBEntry.resolved ? "1" : "0"}"/>`
113
+ const extendedComments =
114
+ this.commentsExtendedXML.query("w15:commentsEx")
115
+ extendedComments.appendXML(extendedString)
116
+ }
117
+ if (!index) {
118
+ options.commentReference = true
119
+ }
120
+ return this.richtext.transformRichtext(node, options)
121
+ })
122
+ .join("")
123
+ string += "</w:comment>"
124
+ commentDBEntry.answers?.forEach(answer => {
125
+ const answerId = ++this.commentIdCounter
126
+ string += `<w:comment w:id="${answerId}" w:author="${escapeText(answer.username)}" w:date="${new Date(answer.date).toISOString().split(".")[0]}Z" w:initials="${escapeText(
127
+ answer.username
128
+ .split(" ")
129
+ .map(n => n[0])
130
+ .join("")
131
+ .toUpperCase()
132
+ )}">`
133
+ string += answer.answer
134
+ .map((node, index) => {
135
+ const options = {section: "CommentText"}
136
+ if (index === answer.answer.length - 1) {
137
+ // We need to add an id to the last paragraph of the comment and add an entry
138
+ // into commentsExtended.xml pointing to the last paragraph of the parent comment.
139
+ const paragraphId = (++this.richtext.paragraphIdCounter)
140
+ .toString(16)
141
+ .padStart(8, "0")
142
+ options.paragraphId = paragraphId
143
+ const extendedString = `<w15:commentEx w15:paraId="${paragraphId}" w15:done="${commentDBEntry.resolved ? "1" : "0"}" w15:paraIdParent="${parentParagraphId}"/>`
144
+ const extendedComments =
145
+ this.commentsExtendedXML.query("w15:commentsEx")
146
+ extendedComments.appendXML(extendedString)
147
+ }
148
+ if (!index) {
149
+ options.commentReference = true
150
+ }
151
+ return this.richtext.transformRichtext(node, options)
152
+ })
153
+ .join("")
154
+ string += "</w:comment>"
155
+ })
156
+ comments.appendXML(string)
157
+ }
158
+
159
+ exportComments() {
160
+ this.usedComments.forEach(comment => {
161
+ this.addComment(comment)
162
+ })
163
+ return Promise.resolve()
164
+ }
165
+ }
@@ -0,0 +1,240 @@
1
+ import {descendantNodes} from "../tools/doc_content.js"
2
+ import {DOCXExporterCitations} from "./citations.js"
3
+ import {DOCXExporterImages} from "./images.js"
4
+ import {DOCXExporterLists} from "./lists.js"
5
+ import {DOCXExporterRels} from "./rels.js"
6
+ import {DOCXExporterRichtext} from "./richtext.js"
7
+
8
+ const DEFAULT_XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
9
+ <w:footnotes xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" mc:Ignorable="w14 wp14">
10
+ <w:footnote w:id="0" w:type="separator">
11
+ <w:p>
12
+ <w:r>
13
+ <w:separator />
14
+ </w:r>
15
+ </w:p>
16
+ </w:footnote>
17
+ <w:footnote w:id="1" w:type="continuationSeparator">
18
+ <w:p>
19
+ <w:r>
20
+ <w:continuationSeparator />
21
+ </w:r>
22
+ </w:p>
23
+ </w:footnote>
24
+ </w:footnotes>`
25
+
26
+ const DEFAULT_SETTINGS_XML = `<w:footnotePr>
27
+ <w:numFmt w:val="decimal"/>
28
+ <w:footnote w:id="0"/>
29
+ <w:footnote w:id="1"/>
30
+ </w:footnotePr>`
31
+
32
+ const DEFAULT_STYLE_FOOTNOTE = `<w:style w:type="paragraph" w:styleId="Footnote">
33
+ <w:name w:val="Footnote Text" />
34
+ <w:basedOn w:val="Normal" />
35
+ <w:pPr>
36
+ <w:suppressLineNumbers />
37
+ <w:ind w:left="339" w:hanging="339" />
38
+ </w:pPr>
39
+ <w:rPr>
40
+ <w:sz w:val="20" />
41
+ <w:szCs w:val="20" />
42
+ </w:rPr>
43
+ </w:style>`
44
+
45
+ const DEFAULT_STYLE_FOOTNOTE_ANCHOR = `
46
+ <w:style w:type="character" w:styleId="FootnoteAnchor">
47
+ <w:name w:val="Footnote Anchor" />
48
+ <w:rPr>
49
+ <w:vertAlign w:val="superscript" />
50
+ </w:rPr>
51
+ </w:style>
52
+ `
53
+
54
+ export class DOCXExporterFootnotes {
55
+ constructor(
56
+ doc,
57
+ docContent,
58
+ settings,
59
+ imageDB,
60
+ bibDB,
61
+ xml,
62
+ citations,
63
+ csl,
64
+ lists,
65
+ math,
66
+ tables,
67
+ rels
68
+ ) {
69
+ this.doc = doc
70
+ this.docContent = docContent
71
+ this.settings = settings
72
+ this.imageDB = imageDB
73
+ this.bibDB = bibDB
74
+ this.xml = xml
75
+ this.citations = citations
76
+ this.csl = csl
77
+ this.lists = lists
78
+ this.math = math
79
+ this.tables = tables
80
+ this.rels = rels
81
+
82
+ this.pmBib = false
83
+ this.fnPmJSON = false
84
+ this.images = false
85
+ this.augmentedCitations = false
86
+ this.footnotes = [] // footnotes
87
+ this.fnXML = false
88
+ this.ctXML = false
89
+ this.styleXML = false
90
+ this.filePath = "word/footnotes.xml"
91
+ this.ctFilePath = "[Content_Types].xml"
92
+ this.settingsFilePath = "word/settings.xml"
93
+ this.styleFilePath = "word/styles.xml"
94
+ }
95
+
96
+ init() {
97
+ this.findFootnotes()
98
+ if (
99
+ this.footnotes.length ||
100
+ (this.citations.citFm.citationType === "note" &&
101
+ this.citations.citInfos.length)
102
+ ) {
103
+ this.convertFootnotes()
104
+ this.fnRels = new DOCXExporterRels(this.xml, "footnotes")
105
+ // Include the citinfos from the main body document so that they will be
106
+ // used for calculating the bibliography as well
107
+ this.augmentedCitations = new DOCXExporterCitations(
108
+ this.fnPmJSON,
109
+ this.settings,
110
+ this.bibDB,
111
+ this.csl,
112
+ this.xml,
113
+ this.citations.citInfos
114
+ )
115
+
116
+ this.images = new DOCXExporterImages(
117
+ this.fnPmJSON,
118
+ this.imageDB,
119
+ this.xml,
120
+ this.fnRels
121
+ )
122
+ this.lists = new DOCXExporterLists(
123
+ this.fnPmJSON,
124
+ this.xml,
125
+ this.fnRels
126
+ )
127
+
128
+ return this.augmentedCitations
129
+ .init()
130
+ .then(() => {
131
+ // Replace the main bibliography with the new one that
132
+ // includes both citations in main document
133
+ // and in the footnotes.
134
+ this.pmBib = this.augmentedCitations.pmBib
135
+ return this.fnRels.init()
136
+ })
137
+ .then(() => this.images.init())
138
+ .then(() => this.lists.init())
139
+ .then(() => this.initCt())
140
+ .then(() => this.setSettings())
141
+ .then(() => this.addStyles())
142
+ .then(() => this.createXml())
143
+ } else {
144
+ // No footnotes were found.
145
+ return Promise.resolve()
146
+ }
147
+ }
148
+
149
+ initCt() {
150
+ return this.xml.getXml(this.ctFilePath).then(ctXML => {
151
+ this.ctXML = ctXML
152
+ this.addRelsToCt()
153
+ return Promise.resolve()
154
+ })
155
+ }
156
+
157
+ addRelsToCt() {
158
+ const override = this.ctXML.query("Override", {
159
+ PartName: `/${this.filePath}`
160
+ })
161
+ if (!override) {
162
+ const types = this.ctXML.query("Types")
163
+ types.appendXML(
164
+ `<Override PartName="/${this.filePath}" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml"/>`
165
+ )
166
+ }
167
+ }
168
+
169
+ addStyles() {
170
+ return this.xml.getXml(this.styleFilePath).then(styleXML => {
171
+ this.styleXML = styleXML
172
+ this.addStyle("Footnote", DEFAULT_STYLE_FOOTNOTE)
173
+ this.addStyle("FootnoteAnchor", DEFAULT_STYLE_FOOTNOTE_ANCHOR)
174
+ return Promise.resolve()
175
+ })
176
+ }
177
+
178
+ addStyle(styleName, xml) {
179
+ if (!this.styleXML.query("w:style", {"w:styleId": styleName})) {
180
+ const stylesEl = this.styleXML.query("w:styles")
181
+ stylesEl.appendXML(xml)
182
+ }
183
+ }
184
+
185
+ findFootnotes() {
186
+ descendantNodes(this.docContent).forEach(node => {
187
+ if (node.type === "footnote") {
188
+ this.footnotes.push(node.attrs.footnote)
189
+ }
190
+ })
191
+ }
192
+
193
+ convertFootnotes() {
194
+ const fnContent = []
195
+ this.footnotes.forEach(footnote => {
196
+ fnContent.push({
197
+ type: "footnotecontainer",
198
+ content: footnote
199
+ })
200
+ })
201
+ this.fnPmJSON = {
202
+ type: "doc",
203
+ content: fnContent
204
+ }
205
+ }
206
+
207
+ createXml() {
208
+ this.richtext = new DOCXExporterRichtext(
209
+ this.doc,
210
+ this.lists,
211
+ this,
212
+ this.settings,
213
+ this.math,
214
+ this.tables,
215
+ this.fnRels,
216
+ this.augmentedCitations,
217
+ this.images
218
+ )
219
+ this.fnXML = this.richtext.transformRichtext(this.fnPmJSON)
220
+ // TODO: add max dimensions
221
+ this.rels.addFootnoteRel()
222
+ return this.xml.getXml(this.filePath, DEFAULT_XML).then(xml => {
223
+ const footnotesEl = xml.query("w:footnotes")
224
+ footnotesEl.appendXML(this.fnXML)
225
+ this.xml = xml
226
+ })
227
+ }
228
+
229
+ setSettings() {
230
+ return this.xml.getXml(this.settingsFilePath).then(settingsXML => {
231
+ const footnotePr = settingsXML.query("w:footnotePr")
232
+ if (!footnotePr) {
233
+ const settingsEl = settingsXML.query("w:settings")
234
+ settingsEl.appendXML(DEFAULT_SETTINGS_XML)
235
+ }
236
+ this.settingsXML = settingsXML
237
+ return Promise.resolve()
238
+ })
239
+ }
240
+ }
@@ -0,0 +1,101 @@
1
+ import {get} from "../../common/index.js"
2
+ import {descendantNodes} from "../tools/doc_content.js"
3
+ import {svg2png} from "../tools/svg.js"
4
+
5
+ export class DOCXExporterImages {
6
+ constructor(docContent, imageDB, xml, rels) {
7
+ this.docContent = docContent
8
+ this.imageDB = imageDB
9
+ this.xml = xml
10
+ this.rels = rels
11
+
12
+ this.images = {}
13
+ this.ctXML = false
14
+ }
15
+
16
+ init() {
17
+ return this.xml.getXml("[Content_Types].xml").then(ctXML => {
18
+ this.ctXML = ctXML
19
+ return this.exportImages()
20
+ })
21
+ }
22
+
23
+ // add an image to the list of files
24
+ addImage(imgFileName, image) {
25
+ const rId = this.rels.addImageRel(imgFileName)
26
+ this.addContentType(imgFileName.split(".").pop())
27
+ this.xml.addExtraFile(`word/media/${imgFileName}`, image)
28
+ return rId
29
+ }
30
+
31
+ // add a global contenttype declaration for an image type (if needed)
32
+ addContentType(fileEnding) {
33
+ const types = this.ctXML.query("Types")
34
+ const contentDec = types.query("Default", {Extension: fileEnding})
35
+ if (!contentDec) {
36
+ const string = `<Default ContentType="image/${fileEnding}" Extension="${fileEnding}"/>`
37
+ types.appendXML(string)
38
+ }
39
+ }
40
+
41
+ // Find all images used in file and add these to the export zip.
42
+ // TODO: This will likely fail on image types docx doesn't support such as SVG.
43
+ // Try out and fix.
44
+ exportImages() {
45
+ const usedImgs = []
46
+ descendantNodes(this.docContent).forEach(node => {
47
+ if (node.type === "image" && node.attrs.image !== false) {
48
+ if (!usedImgs.includes(node.attrs.image)) {
49
+ usedImgs.push(node.attrs.image)
50
+ }
51
+ }
52
+ })
53
+ return new Promise(resolveExportImages => {
54
+ const p = []
55
+ usedImgs.forEach(image => {
56
+ const imgDBEntry = this.imageDB.db[image]
57
+ p.push(
58
+ get(imgDBEntry.image)
59
+ .then(response => response.blob())
60
+ .then(blob => {
61
+ if (blob.type === "image/svg+xml") {
62
+ // DOCX doesn't support SVG. Convert to PNG.
63
+ return svg2png(blob).then(
64
+ ({blob: pngBlob, width, height}) => {
65
+ const wImgId = this.addImage(
66
+ imgDBEntry.image
67
+ .split("/")
68
+ .pop()
69
+ .replace(/.svg$/g, ".png"),
70
+ pngBlob
71
+ )
72
+ this.images[image] = {
73
+ id: wImgId,
74
+ width,
75
+ height,
76
+ title: imgDBEntry.title
77
+ }
78
+ }
79
+ )
80
+ } else {
81
+ const wImgId = this.addImage(
82
+ imgDBEntry.image.split("/").pop(),
83
+ blob
84
+ )
85
+ this.images[image] = {
86
+ id: wImgId,
87
+ width: imgDBEntry.width,
88
+ height: imgDBEntry.height,
89
+ title: imgDBEntry.title
90
+ }
91
+ }
92
+ })
93
+ )
94
+ })
95
+
96
+ Promise.all(p).then(() => {
97
+ resolveExportImages()
98
+ })
99
+ })
100
+ }
101
+ }
@@ -0,0 +1,185 @@
1
+ import download from "downloadjs"
2
+
3
+ import {shortFileTitle} from "../../common/index.js"
4
+ import {fixTables, removeHidden, textContent} from "../tools/doc_content.js"
5
+ import {createSlug} from "../tools/file.js"
6
+ import {XmlZip} from "../tools/xml_zip.js"
7
+ import {DOCXExporterCitations} from "./citations.js"
8
+ import {DOCXExporterComments} from "./comments.js"
9
+ import {DOCXExporterFootnotes} from "./footnotes.js"
10
+ import {DOCXExporterImages} from "./images.js"
11
+ import {DOCXExporterLists} from "./lists.js"
12
+ import {DOCXExporterMath} from "./math.js"
13
+ import {DOCXExporterMetadata} from "./metadata.js"
14
+ import {DOCXExporterRels} from "./rels.js"
15
+ import {DOCXExporterRender} from "./render.js"
16
+ import {DOCXExporterRichtext} from "./richtext.js"
17
+ import {DOCXExporterTables} from "./tables.js"
18
+ import {moveFootnoteComments} from "./tools.js"
19
+
20
+ /*
21
+ Exporter to Office Open XML docx (Microsoft Word)
22
+ */
23
+
24
+ /*
25
+ TODO:
26
+ * - Remove comments
27
+ * - Export document language
28
+ * - Templating of tag/contributor output
29
+ */
30
+
31
+ export class DOCXExporter {
32
+ constructor(doc, templateUrl, bibDB, imageDB, csl) {
33
+ this.doc = doc
34
+ this.templateUrl = templateUrl
35
+ this.bibDB = bibDB
36
+ this.imageDB = imageDB
37
+ this.csl = csl
38
+
39
+ this.docTitle = shortFileTitle(this.doc.title, this.doc.path)
40
+ this.mimeType =
41
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
42
+ this.docContent = moveFootnoteComments(
43
+ fixTables(removeHidden(this.doc.content))
44
+ )
45
+ }
46
+
47
+ init() {
48
+ const xml = new XmlZip(this.templateUrl, this.mimeType)
49
+
50
+ const tables = new DOCXExporterTables(xml)
51
+ const math = new DOCXExporterMath(xml)
52
+ const render = new DOCXExporterRender(xml)
53
+ const rels = new DOCXExporterRels(xml, "document")
54
+ const metadata = new DOCXExporterMetadata(
55
+ xml,
56
+ this.getBaseMetadata(),
57
+ this.csl
58
+ )
59
+
60
+ const images = new DOCXExporterImages(
61
+ this.docContent,
62
+ this.imageDB,
63
+ xml,
64
+ rels
65
+ )
66
+ const lists = new DOCXExporterLists(this.docContent, xml, rels)
67
+ const citations = new DOCXExporterCitations(
68
+ this.docContent,
69
+ this.doc.settings,
70
+ this.bibDB,
71
+ this.csl,
72
+ xml
73
+ )
74
+
75
+ const footnotes = new DOCXExporterFootnotes(
76
+ this.doc,
77
+ this.docContent,
78
+ this.doc.settings,
79
+ this.imageDB,
80
+ this.bibDB,
81
+ xml,
82
+ citations,
83
+ this.csl,
84
+ lists,
85
+ math,
86
+ tables,
87
+ rels
88
+ )
89
+
90
+ const richtext = new DOCXExporterRichtext(
91
+ this.doc,
92
+ this.doc.settings,
93
+ lists,
94
+ footnotes,
95
+ math,
96
+ tables,
97
+ rels,
98
+ citations,
99
+ images
100
+ )
101
+
102
+ const comments = new DOCXExporterComments(
103
+ this.docContent,
104
+ this.doc.comments,
105
+ xml,
106
+ rels,
107
+ richtext
108
+ )
109
+
110
+ return xml
111
+ .init()
112
+ .then(() => citations.init())
113
+ .then(() => metadata.init())
114
+ .then(() => tables.init())
115
+ .then(() => math.init())
116
+ .then(() => render.init())
117
+ .then(() => rels.init())
118
+ .then(() => images.init())
119
+ .then(() => comments.init())
120
+ .then(() => lists.init())
121
+ .then(() => footnotes.init())
122
+ .then(() => {
123
+ const pmBib = footnotes.pmBib || citations.pmBib
124
+ render.render(
125
+ this.docContent,
126
+ pmBib,
127
+ this.doc.settings,
128
+ richtext,
129
+ citations
130
+ )
131
+ return xml.prepareBlob()
132
+ })
133
+ .then(blob => this.download(blob))
134
+ }
135
+
136
+ download(blob) {
137
+ return download(
138
+ blob,
139
+ createSlug(this.docTitle) + ".docx",
140
+ this.mimeType
141
+ )
142
+ }
143
+
144
+ getBaseMetadata() {
145
+ const contributors = this.docContent.content.reduce(
146
+ (contributors, part) => {
147
+ if (
148
+ part.type === "contributors_part" &&
149
+ part.attrs.metadata &&
150
+ part.content
151
+ ) {
152
+ return contributors.concat(
153
+ part.content.map(node => ({
154
+ ...node.attrs,
155
+ role: part.attrs.metadata
156
+ }))
157
+ )
158
+ } else {
159
+ return contributors
160
+ }
161
+ },
162
+ []
163
+ )
164
+ return {
165
+ authors: contributors.filter(c => c.role === "authors"),
166
+ contributors,
167
+ keywords: this.docContent.content.reduce((keywords, part) => {
168
+ if (
169
+ part.type === "tags_part" &&
170
+ part.attrs.metadata === "keywords" &&
171
+ part.content
172
+ ) {
173
+ return keywords.concat(
174
+ part.content.map(keywordNode => keywordNode.attrs.tag)
175
+ )
176
+ } else {
177
+ return keywords
178
+ }
179
+ }, []),
180
+ title: textContent(this.docContent.content[0]),
181
+ language: this.doc.settings.language,
182
+ citationStyle: this.doc.settings.citationstyle
183
+ }
184
+ }
185
+ }