@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,21 @@
1
+ export const escapeLatexText = text =>
2
+ text
3
+ // Remove line breaks
4
+ .replace(/\r|\n/g, "")
5
+ // Escape characters that are protected in some way.
6
+ .replace(/\{/g, "\\{")
7
+ .replace(/\}/g, "\\}")
8
+ .replace(/\^/g, "\\textasciicircum{}")
9
+ .replace(/\$/g, "\\$")
10
+ .replace(/_/g, "\\_")
11
+ .replace(/~/g, "\\textasciitilde{}")
12
+ .replace(/#/g, "\\#")
13
+ .replace(/%/g, "\\%")
14
+ .replace(/&/g, "\\&")
15
+ .replace(/\\\\/g, "\\textbackslash")
16
+
17
+ // Remove control characters that somehow have ended up in the document
18
+ .replace(/\u000B/g, "")
19
+ .replace(/\u000C/g, "")
20
+ .replace(/\u000E/g, "")
21
+ .replace(/\u000F/g, "")
@@ -0,0 +1,74 @@
1
+ import {BibLatexExporter} from "biblatex-csl-converter"
2
+ import download from "downloadjs"
3
+
4
+ import {shortFileTitle} from "../../common/index.js"
5
+ import {fixTables, removeHidden} from "../tools/doc_content.js"
6
+ import {createSlug} from "../tools/file.js"
7
+ import {ZipFileCreator} from "../tools/zip.js"
8
+ import {LatexExporterConvert} from "./convert.js"
9
+ import {readMe} from "./readme.js"
10
+ /*
11
+ Exporter to LaTeX
12
+ */
13
+
14
+ export class LatexExporter {
15
+ constructor(doc, bibDB, imageDB, updated) {
16
+ this.doc = doc
17
+ this.docTitle = shortFileTitle(this.doc.title, this.doc.path)
18
+ this.bibDB = bibDB
19
+ this.imageDB = imageDB
20
+ this.updated = updated
21
+
22
+ this.docContent = false
23
+ this.zipFileName = false
24
+ this.textFiles = []
25
+ this.httpFiles = []
26
+ }
27
+
28
+ init() {
29
+ this.zipFileName = `${createSlug(this.docTitle)}.latex.zip`
30
+ this.docContent = fixTables(removeHidden(this.doc.content))
31
+ this.converter = new LatexExporterConvert(
32
+ this,
33
+ this.imageDB,
34
+ this.bibDB,
35
+ this.doc.settings
36
+ )
37
+ this.conversion = this.converter.init(this.docContent)
38
+ if (Object.keys(this.conversion.usedBibDB).length > 0) {
39
+ const bibExport = new BibLatexExporter(this.conversion.usedBibDB)
40
+ this.textFiles.push({
41
+ filename: "bibliography.bib",
42
+ contents: bibExport.parse()
43
+ })
44
+ }
45
+ this.textFiles.push({
46
+ filename: "document.tex",
47
+ contents: this.conversion.latex
48
+ })
49
+ this.textFiles.push({filename: "README.txt", contents: readMe})
50
+ this.conversion.imageIds.forEach(id => {
51
+ this.httpFiles.push({
52
+ filename: this.imageDB.db[id].image.split("/").pop(),
53
+ url: this.imageDB.db[id].image
54
+ })
55
+ })
56
+ return this.createZip()
57
+ }
58
+
59
+ createZip() {
60
+ const zipper = new ZipFileCreator(
61
+ this.textFiles,
62
+ this.httpFiles,
63
+ undefined,
64
+ undefined,
65
+ this.updated
66
+ )
67
+
68
+ return zipper.init().then(blob => this.download(blob))
69
+ }
70
+
71
+ download(blob) {
72
+ return download(blob, this.zipFileName, "application/zip")
73
+ }
74
+ }
@@ -0,0 +1,22 @@
1
+ export const readMe = `We strongly recommend using LuaTeX instead of PDFTeX or XeLaTeX for document
2
+ compilation.
3
+
4
+ In order to compile the LaTeX file, you need to use at least TeXLive 2016. If
5
+ there are citations, you additionally need Biber 2.7/BibLaTeX 3.7.
6
+
7
+ On Ubuntu 18.04+ install these packages:
8
+
9
+ > sudo apt install texlive-latex-base texlive-bibtex-extra biber texlive-latex-extra
10
+
11
+ Extract all the files included in this ZIP into a folder.
12
+ Run then these commands to create a PDF from within this folder:
13
+
14
+ > lualatex document
15
+
16
+ If there are citations, continue with these commands:
17
+
18
+ > biber document
19
+ > lualatex document
20
+
21
+ Look at the output messages to determine whether you need to run laluatex again.
22
+ `
@@ -0,0 +1,132 @@
1
+ import {addAlert} from "../../common/index.js"
2
+ import {docSchema} from "../../schema/document/index.js"
3
+ import {toMiniJSON} from "../../schema/mini_json.js"
4
+ // Generate a copy of the fidus doc, imageDB and bibDB with all clutter removed.
5
+ export class ShrinkFidus {
6
+ /**
7
+ * @param {Object} doc - Full document object.
8
+ * @param {Object} imageDB - Image database, e.g. {db: {...}}.
9
+ * @param {Object} bibDB - Bibliography database, e.g. {db: {...}}.
10
+ * @param {boolean} [silent=false] - When true, suppresses the
11
+ * "File export has been initiated" info alert. Pass true when
12
+ * shrinking multiple documents in a loop (e.g. one per book chapter)
13
+ * and the caller already shows its own progress notification.
14
+ */
15
+ constructor(doc, imageDB, bibDB, silent = false) {
16
+ this.doc = doc
17
+ this.imageDB = imageDB
18
+ this.bibDB = bibDB
19
+ this.silent = silent
20
+ this.imageList = []
21
+ this.citeList = []
22
+ }
23
+
24
+ init() {
25
+ const shrunkImageDB = {},
26
+ httpIncludes = []
27
+
28
+ if (!this.silent) {
29
+ addAlert("info", gettext("File export has been initiated."))
30
+ }
31
+
32
+ this.walkTree(this.doc.content)
33
+
34
+ this.imageList = [...new Set(this.imageList)] // unique values
35
+
36
+ this.imageList.forEach(itemId => {
37
+ shrunkImageDB[itemId] = Object.assign({}, this.imageDB.db[itemId])
38
+ // Remove parts that are connected to a particular user/server
39
+ delete shrunkImageDB[itemId].cats
40
+ delete shrunkImageDB[itemId].thumbnail
41
+ delete shrunkImageDB[itemId].pk
42
+ delete shrunkImageDB[itemId].added
43
+ const imageUrl = shrunkImageDB[itemId].image
44
+ let filename
45
+ if (imageUrl.startsWith("blob:")) {
46
+ // Blob URL produced by decrypting an E2EE image client-side.
47
+ // The URL itself carries no useful file extension, so derive
48
+ // one from the image entry's MIME type instead. Without this
49
+ // the server rejects the upload because get_encrypted_file_path
50
+ // requires a recognised extension.
51
+ const mime = shrunkImageDB[itemId].file_type || "image/png"
52
+ const mimeExtMap = {
53
+ "image/png": "png",
54
+ "image/jpeg": "jpg",
55
+ "image/jpg": "jpg",
56
+ "image/webp": "webp",
57
+ "image/svg+xml": "svg",
58
+ "image/gif": "gif",
59
+ "image/avif": "avif"
60
+ }
61
+ const ext = mimeExtMap[mime] || "png"
62
+ filename = `images/${itemId}.${ext}`
63
+ } else {
64
+ filename = `images/${imageUrl.split("/").pop()}`
65
+ }
66
+ shrunkImageDB[itemId].image = filename
67
+ httpIncludes.push({
68
+ url: imageUrl,
69
+ filename
70
+ })
71
+ })
72
+
73
+ this.citeList = [...new Set(this.citeList)] // unique values
74
+
75
+ const shrunkBibDB = {}
76
+ this.citeList.forEach(itemId => {
77
+ shrunkBibDB[itemId] = Object.assign({}, this.bibDB.db[itemId])
78
+ // Remove the cats, as it is only a list of IDs for one
79
+ // particular user/server.
80
+ delete shrunkBibDB[itemId].cats
81
+ })
82
+
83
+ const docCopy = Object.assign({}, this.doc)
84
+
85
+ // Remove items that aren't needed.
86
+ delete docCopy.rights
87
+ delete docCopy.version
88
+ delete docCopy.comment_version
89
+ delete docCopy.owner
90
+ delete docCopy.id
91
+ delete docCopy.is_owner
92
+ delete docCopy.added
93
+ delete docCopy.updated
94
+ delete docCopy.revisions
95
+
96
+ docCopy.content = toMiniJSON(docSchema.nodeFromJSON(docCopy.content))
97
+
98
+ return new Promise(resolve =>
99
+ resolve({
100
+ doc: docCopy,
101
+ shrunkImageDB,
102
+ shrunkBibDB,
103
+ httpIncludes
104
+ })
105
+ )
106
+ }
107
+
108
+ walkTree(node) {
109
+ switch (node.type) {
110
+ case "citation":
111
+ this.citeList = this.citeList.concat(
112
+ node.attrs.references.map(ref => ref.id)
113
+ )
114
+ break
115
+ case "image":
116
+ if (node.attrs.image !== false) {
117
+ this.imageList.push(node.attrs.image)
118
+ }
119
+ break
120
+ case "footnote":
121
+ if (node.attrs?.footnote) {
122
+ node.attrs.footnote.forEach(childNode =>
123
+ this.walkTree(childNode)
124
+ )
125
+ }
126
+ break
127
+ }
128
+ if (node.content) {
129
+ node.content.forEach(childNode => this.walkTree(childNode))
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,101 @@
1
+ import {DOMParser, DOMSerializer} from "prosemirror-model"
2
+
3
+ import {cslBibSchema} from "../../bibliography/csl_bib.js"
4
+ import {FormatCitations} from "../../citations/format.js"
5
+ import {fnSchema} from "../../schema/footnotes.js"
6
+ import {descendantNodes} from "../tools/doc_content.js"
7
+
8
+ export class ODTExporterCitations {
9
+ constructor(docContent, settings, styles, bibDB, csl, origCitInfos = []) {
10
+ this.docContent = docContent
11
+ this.settings = settings
12
+ this.styles = styles
13
+ this.bibDB = bibDB
14
+ this.csl = csl
15
+ // If citInfos were found in a previous run, they are stored here
16
+ // (for example: first citations in main document, then in footnotes)
17
+ this.origCitInfos = origCitInfos
18
+ this.citInfos = []
19
+ this.citationTexts = []
20
+ this.pmCits = []
21
+ this.citFm = false
22
+ this.pmBib = false
23
+ }
24
+
25
+ init() {
26
+ return this.formatCitations()
27
+ }
28
+
29
+ // Citations are highly interdependent -- so we need to format them all
30
+ // together before laying out the document.
31
+ formatCitations() {
32
+ if (this.origCitInfos.length) {
33
+ // Initial citInfos are taken from a previous run to include in
34
+ // bibliography, and they are removed before spitting out the
35
+ // citation entries for the given document.
36
+ // That way the bibliography should contain information from both.
37
+ this.citInfos = this.citInfos.concat(this.origCitInfos)
38
+ }
39
+
40
+ descendantNodes(this.docContent).forEach(node => {
41
+ if (node.type === "citation") {
42
+ this.citInfos.push(JSON.parse(JSON.stringify(node.attrs)))
43
+ }
44
+ })
45
+ this.citFm = new FormatCitations(
46
+ this.csl,
47
+ this.citInfos,
48
+ this.settings.citationstyle,
49
+ "",
50
+ this.bibDB,
51
+ false,
52
+ this.settings.language
53
+ )
54
+ return this.citFm.init().then(() => {
55
+ this.citationTexts = this.citFm.citationTexts
56
+ if (this.origCitInfos.length) {
57
+ // Remove all citation texts originating from original starting citInfos
58
+ this.citationTexts.splice(0, this.origCitInfos.length)
59
+ }
60
+ this.convertCitations()
61
+ return Promise.resolve()
62
+ })
63
+ }
64
+
65
+ convertCitations() {
66
+ // There could be some formatting in the citations, so we parse them through the PM schema for final formatting.
67
+ // We need to put the citations each in a paragraph so that it works with
68
+ // the fiduswriter schema and so that the converter doesn't mash them together.
69
+ if (this.citationTexts.length) {
70
+ let citationsHTML = ""
71
+ this.citationTexts.forEach(ct => {
72
+ citationsHTML += `<p>${ct}</p>`
73
+ })
74
+
75
+ // We create a standard footnote container DOM node,
76
+ // add the citations into it, and parse it back.
77
+ const fnNode = fnSchema.nodeFromJSON({type: "footnotecontainer"})
78
+ const serializer = DOMSerializer.fromSchema(fnSchema)
79
+ const dom = serializer.serializeNode(fnNode)
80
+ dom.innerHTML = citationsHTML
81
+ this.pmCits = DOMParser.fromSchema(fnSchema)
82
+ .parse(dom, {topNode: fnNode})
83
+ .toJSON().content
84
+ } else {
85
+ this.pmCits = []
86
+ }
87
+
88
+ // Now we do the same for the bibliography.
89
+ const cslBib = this.citFm.bibliography
90
+ if (cslBib && cslBib[1].length > 0) {
91
+ this.styles.addReferenceStyle(cslBib[0])
92
+ const bibNode = cslBibSchema.nodeFromJSON({type: "cslbib"})
93
+ const serializer = DOMSerializer.fromSchema(cslBibSchema)
94
+ const dom = serializer.serializeNode(bibNode)
95
+ dom.innerHTML = cslBib[1].join("")
96
+ this.pmBib = DOMParser.fromSchema(cslBibSchema)
97
+ .parse(dom, {topNode: bibNode})
98
+ .toJSON()
99
+ }
100
+ }
101
+ }
@@ -0,0 +1,147 @@
1
+ import {descendantNodes} from "../tools/doc_content.js"
2
+ import {ODTExporterCitations} from "./citations.js"
3
+ import {ODTExporterImages} from "./images.js"
4
+
5
+ const DEFAULT_STYLE_FOOTNOTE = `
6
+ <style:style style:name="Footnote" style:family="paragraph" style:parent-style-name="Standard" style:class="extra">
7
+ <style:paragraph-properties fo:margin-left="0.2354in" fo:margin-right="0in" fo:text-indent="-0.2354in" style:auto-text-indent="false" text:number-lines="false" text:line-number="0" />
8
+ <style:text-properties fo:font-size="10pt" style:font-size-asian="10pt" style:font-size-complex="10pt" />
9
+ </style:style>
10
+ `
11
+
12
+ const DEFAULT_STYLE_FOOTNOTE_ANCHOR = `
13
+ <style:style style:name="Footnote_20_anchor" style:display-name="Footnote anchor" style:family="text">
14
+ <style:text-properties style:text-position="super 58%" />
15
+ </style:style>
16
+ `
17
+ const DEFAULT_STYLE_FOOTNOTE_SYMBOL = `
18
+ <style:style style:name="Footnote_20_Symbol" style:display-name="Footnote Symbol" style:family="text" />
19
+ `
20
+
21
+ const DEFAULT_STYLE_FOOTNOTE_CONFIGURATION = `
22
+ <text:notes-configuration text:note-class="footnote" text:citation-style-name="Footnote_20_Symbol" text:citation-body-style-name="Footnote_20_anchor" style:num-format="1" text:start-value="0" text:footnotes-position="page" text:start-numbering-at="document" />
23
+ `
24
+
25
+ export class ODTExporterFootnotes {
26
+ constructor(
27
+ docContent,
28
+ settings,
29
+ xml,
30
+ bodyCitations,
31
+ styles,
32
+ bibDB,
33
+ imageDB,
34
+ csl
35
+ ) {
36
+ this.docContent = docContent
37
+ this.settings = settings
38
+ this.xml = xml
39
+ this.bodyCitations = bodyCitations
40
+ this.styles = styles
41
+ this.bibDB = bibDB
42
+ this.imageDB = imageDB
43
+ this.csl = csl
44
+
45
+ this.pmBib = false
46
+ this.fnPmJSON = false
47
+ this.images = false
48
+ this.citations = false
49
+ this.footnotes = []
50
+ this.styleFilePath = "styles.xml"
51
+ }
52
+
53
+ init() {
54
+ this.findFootnotes()
55
+ if (
56
+ this.footnotes.length ||
57
+ (this.bodyCitations.citFm.citationType === "note" &&
58
+ this.bodyCitations.citInfos.length)
59
+ ) {
60
+ this.convertFootnotes()
61
+ // Include the citinfos from the main document so that they will be
62
+ // used for calculating the bibliography as well
63
+ this.citations = new ODTExporterCitations(
64
+ this.fnPmJSON,
65
+ this.settings,
66
+ this.styles,
67
+ this.bibDB,
68
+ this.csl,
69
+ this.bodyCitations.citInfos
70
+ )
71
+ this.images = new ODTExporterImages(
72
+ this.fnPmJSON,
73
+ this.xml,
74
+ this.imageDB
75
+ )
76
+
77
+ return this.citations
78
+ .init()
79
+ .then(() => {
80
+ // Replace the main bibliography with the new one that includes
81
+ // both citations in main document and in the footnotes.
82
+ this.pmBib = this.citations.pmBib
83
+ return this.images.init()
84
+ })
85
+ .then(() => {
86
+ return this.addStyles()
87
+ })
88
+ } else {
89
+ // No footnotes were found.
90
+ return Promise.resolve()
91
+ }
92
+ }
93
+
94
+ addStyles() {
95
+ return this.xml.getXml(this.styleFilePath).then(styleXml => {
96
+ this.styleXml = styleXml
97
+ this.addStyle("Footnote", DEFAULT_STYLE_FOOTNOTE)
98
+ this.addStyle("Footnote_20_anchor", DEFAULT_STYLE_FOOTNOTE_ANCHOR)
99
+ this.addStyle("Footnote_20_Symbol", DEFAULT_STYLE_FOOTNOTE_SYMBOL)
100
+ this.setStyleConfig()
101
+ return Promise.resolve()
102
+ })
103
+ }
104
+
105
+ addStyle(styleName, xml) {
106
+ if (!this.styleXml.query("style:style", {"style:name": styleName})) {
107
+ const stylesEl = this.styleXml.query("office:styles")
108
+ stylesEl.appendXML(xml)
109
+ }
110
+ }
111
+
112
+ setStyleConfig() {
113
+ const oldFnStyleConfigEl = this.styleXml.query(
114
+ "text:notes-configuration",
115
+ {
116
+ "text:note-class": "footnote"
117
+ }
118
+ )
119
+ if (oldFnStyleConfigEl) {
120
+ oldFnStyleConfigEl.parentElement.removeChild(oldFnStyleConfigEl)
121
+ }
122
+ const stylesEl = this.styleXml.query("office:styles")
123
+ stylesEl.appendXML(DEFAULT_STYLE_FOOTNOTE_CONFIGURATION)
124
+ }
125
+
126
+ findFootnotes() {
127
+ descendantNodes(this.docContent).forEach(node => {
128
+ if (node.type === "footnote") {
129
+ this.footnotes.push(node.attrs.footnote)
130
+ }
131
+ })
132
+ }
133
+
134
+ convertFootnotes() {
135
+ const fnContent = []
136
+ this.footnotes.forEach(footnote => {
137
+ fnContent.push({
138
+ type: "footnotecontainer",
139
+ content: footnote
140
+ })
141
+ })
142
+ this.fnPmJSON = {
143
+ type: "doc",
144
+ content: fnContent
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,115 @@
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 ODTExporterImages {
6
+ constructor(docContent, xml, imageDB) {
7
+ this.docContent = docContent
8
+ this.xml = xml
9
+ this.imageDB = imageDB
10
+ this.images = {}
11
+ this.manifestXml = false
12
+ }
13
+
14
+ init() {
15
+ return this.xml.getXml("META-INF/manifest.xml").then(manifestXml => {
16
+ this.manifestXml = manifestXml
17
+ return this.exportImages()
18
+ })
19
+ }
20
+
21
+ // add an image to the list of files
22
+ addImage(imgFileName, image) {
23
+ imgFileName = this.addFileToManifest(imgFileName)
24
+ this.xml.addExtraFile(`Pictures/${imgFileName}`, image)
25
+ return imgFileName
26
+ }
27
+
28
+ // add a an image file to the manifest
29
+ addFileToManifest(imgFileName) {
30
+ const fileNameParts = imgFileName.split(".")
31
+ const fileNameEnding = fileNameParts.pop()
32
+ const fileNameStart = fileNameParts.join(".")
33
+ const manifestEl = this.manifestXml.query("manifest:manifest")
34
+ let imgManifest = manifestEl.query("manifest:file-entry", {
35
+ "manifest:full-path": `Pictures/${imgFileName}`
36
+ })
37
+ let counter = 0
38
+ while (imgManifest) {
39
+ // Name exists already, we change the name until we get a file name not yet included in manifest.
40
+ imgFileName = `${fileNameStart}_${counter++}.${fileNameEnding}`
41
+ imgManifest = manifestEl.query("manifest:file-entry", {
42
+ "manifest:full-path": `Pictures/${imgFileName}`
43
+ })
44
+ }
45
+ const string = ` <manifest:file-entry manifest:full-path="Pictures/${imgFileName}" manifest:media-type="image/${fileNameEnding}"/>`
46
+ manifestEl.appendXML(string)
47
+ return imgFileName
48
+ }
49
+
50
+ // Find all images used in file and add these to the export zip.
51
+ // TODO: This will likely fail on image types odt doesn't support such as
52
+ // SVG. Try out and fix.
53
+ exportImages() {
54
+ const usedImgs = []
55
+
56
+ descendantNodes(this.docContent).forEach(node => {
57
+ if (node.type === "image" && node.attrs.image !== false) {
58
+ if (!usedImgs.includes(node.attrs.image)) {
59
+ usedImgs.push(node.attrs.image)
60
+ }
61
+ }
62
+ })
63
+
64
+ return new Promise(resolveExportImages => {
65
+ const p = []
66
+
67
+ usedImgs.forEach(image => {
68
+ const imgDBEntry = this.imageDB.db[image]
69
+ p.push(
70
+ get(imgDBEntry.image)
71
+ .then(response => response.blob())
72
+ .then(blob => {
73
+ const wImgId = this.addImage(
74
+ imgDBEntry.image.split("/").pop(),
75
+ blob
76
+ )
77
+ if (blob.type === "image/svg+xml") {
78
+ // Add PNG version in addition to SVG
79
+ return svg2png(blob).then(
80
+ ({blob: pngBlob, width, height}) => {
81
+ const pngWImgId = this.addImage(
82
+ imgDBEntry.image
83
+ .split("/")
84
+ .pop()
85
+ .replace(/.svg$/g, ".png"),
86
+ pngBlob
87
+ )
88
+ this.images[image] = {
89
+ id: pngWImgId,
90
+ width,
91
+ height,
92
+ title: imgDBEntry.title,
93
+ type: blob.type,
94
+ svg: wImgId
95
+ }
96
+ }
97
+ )
98
+ } else {
99
+ this.images[image] = {
100
+ id: wImgId,
101
+ width: imgDBEntry.width,
102
+ height: imgDBEntry.height,
103
+ title: imgDBEntry.title,
104
+ type: blob.type,
105
+ svg: false
106
+ }
107
+ }
108
+ })
109
+ )
110
+ })
111
+
112
+ Promise.all(p).then(() => resolveExportImages())
113
+ })
114
+ }
115
+ }