@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.
- package/LICENSE +661 -0
- package/README.md +16 -0
- package/jest.config.js +23 -0
- package/package.json +59 -0
- package/schema.json +1 -0
- package/scripts/export-schema.js +16 -0
- package/src/bibliography/common.js +92 -0
- package/src/bibliography/csl_bib.js +139 -0
- package/src/citations/citeproc_sys.js +42 -0
- package/src/citations/format.js +194 -0
- package/src/common/blob.js +10 -0
- package/src/common/file.js +25 -0
- package/src/common/index.js +12 -0
- package/src/common/network.js +79 -0
- package/src/common/text.js +44 -0
- package/src/editor/e2ee/encryptor.js +228 -0
- package/src/exporter/docx/citations.js +177 -0
- package/src/exporter/docx/comments.js +165 -0
- package/src/exporter/docx/footnotes.js +240 -0
- package/src/exporter/docx/images.js +101 -0
- package/src/exporter/docx/index.js +185 -0
- package/src/exporter/docx/lists.js +260 -0
- package/src/exporter/docx/math.js +46 -0
- package/src/exporter/docx/metadata.js +289 -0
- package/src/exporter/docx/rels.js +193 -0
- package/src/exporter/docx/render.js +941 -0
- package/src/exporter/docx/richtext.js +1182 -0
- package/src/exporter/docx/tables.js +112 -0
- package/src/exporter/docx/tools.js +50 -0
- package/src/exporter/epub/index.js +142 -0
- package/src/exporter/epub/templates.js +140 -0
- package/src/exporter/epub/tools.js +96 -0
- package/src/exporter/html/citations.js +121 -0
- package/src/exporter/html/convert.js +813 -0
- package/src/exporter/html/index.js +192 -0
- package/src/exporter/html/templates.js +34 -0
- package/src/exporter/html/tools.js +50 -0
- package/src/exporter/jats/bibliography.js +183 -0
- package/src/exporter/jats/citations.js +109 -0
- package/src/exporter/jats/convert.js +871 -0
- package/src/exporter/jats/index.js +92 -0
- package/src/exporter/jats/templates.js +35 -0
- package/src/exporter/jats/text.js +72 -0
- package/src/exporter/latex/convert.js +934 -0
- package/src/exporter/latex/escape_latex.js +21 -0
- package/src/exporter/latex/index.js +74 -0
- package/src/exporter/latex/readme.js +22 -0
- package/src/exporter/native/shrink.js +132 -0
- package/src/exporter/odt/citations.js +101 -0
- package/src/exporter/odt/footnotes.js +147 -0
- package/src/exporter/odt/images.js +115 -0
- package/src/exporter/odt/index.js +156 -0
- package/src/exporter/odt/math.js +57 -0
- package/src/exporter/odt/metadata.js +251 -0
- package/src/exporter/odt/render.js +806 -0
- package/src/exporter/odt/richtext.js +865 -0
- package/src/exporter/odt/styles.js +387 -0
- package/src/exporter/odt/track.js +68 -0
- package/src/exporter/pandoc/citations.js +98 -0
- package/src/exporter/pandoc/convert.js +1017 -0
- package/src/exporter/pandoc/index.js +92 -0
- package/src/exporter/pandoc/readme.js +8 -0
- package/src/exporter/pandoc/tools.js +51 -0
- package/src/exporter/print/index.js +177 -0
- package/src/exporter/tools/doc_content.js +144 -0
- package/src/exporter/tools/file.js +9 -0
- package/src/exporter/tools/json.js +73 -0
- package/src/exporter/tools/svg.js +29 -0
- package/src/exporter/tools/xml.js +531 -0
- package/src/exporter/tools/xml_zip.js +95 -0
- package/src/exporter/tools/zip.js +90 -0
- package/src/exporter/tools/zotero_csl.js +93 -0
- package/src/importer/citations.js +129 -0
- package/src/importer/docx/citations.js +123 -0
- package/src/importer/docx/convert.js +1427 -0
- package/src/importer/docx/helpers.js +9 -0
- package/src/importer/docx/omml2mathml.js +1448 -0
- package/src/importer/docx/parse.js +735 -0
- package/src/importer/native/get_images.js +76 -0
- package/src/importer/native/update.js +29 -0
- package/src/importer/odt/citations.js +87 -0
- package/src/importer/odt/convert.js +1855 -0
- package/src/importer/pandoc/convert.js +884 -0
- package/src/importer/pandoc/helpers.js +84 -0
- package/src/importer/zip_analyzer.js +102 -0
- package/src/index.js +1 -0
- package/src/mathlive/opf_includes.js +24 -0
- package/src/schema/common/annotate.js +76 -0
- package/src/schema/common/base.js +118 -0
- package/src/schema/common/citation.js +62 -0
- package/src/schema/common/equation.js +31 -0
- package/src/schema/common/figure.js +190 -0
- package/src/schema/common/heading.js +43 -0
- package/src/schema/common/index.js +40 -0
- package/src/schema/common/list.js +95 -0
- package/src/schema/common/reference.js +100 -0
- package/src/schema/common/table.js +103 -0
- package/src/schema/common/track.js +190 -0
- package/src/schema/const.js +58 -0
- package/src/schema/convert.js +1272 -0
- package/src/schema/document/content.js +187 -0
- package/src/schema/document/index.js +117 -0
- package/src/schema/document/structure.js +452 -0
- package/src/schema/export.js +21 -0
- package/src/schema/footnotes.js +126 -0
- package/src/schema/footnotes_convert.js +31 -0
- package/src/schema/i18n.js +595 -0
- package/src/schema/index.js +5 -0
- package/src/schema/mini_json.js +61 -0
- 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
|
+
}
|