@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,79 @@
|
|
|
1
|
+
const getCookie = name => {
|
|
2
|
+
if (typeof document === "undefined" || !document.cookie) {
|
|
3
|
+
return ""
|
|
4
|
+
}
|
|
5
|
+
const cookie = document.cookie
|
|
6
|
+
.split(";")
|
|
7
|
+
.map(cookie => cookie.trim())
|
|
8
|
+
.find(cookie => cookie.substring(0, name.length + 1) === `${name}=`)
|
|
9
|
+
return cookie ? decodeURIComponent(cookie.substring(name.length + 1)) : ""
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const handleFetchErrors = response => {
|
|
13
|
+
if (!response.ok) {
|
|
14
|
+
throw response
|
|
15
|
+
}
|
|
16
|
+
return response
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const get = (url, params = {}) => {
|
|
20
|
+
const csrfToken = getCookie("csrftoken")
|
|
21
|
+
const queryString = Object.keys(params)
|
|
22
|
+
.map(
|
|
23
|
+
key =>
|
|
24
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`
|
|
25
|
+
)
|
|
26
|
+
.join("&")
|
|
27
|
+
if (queryString.length) {
|
|
28
|
+
url = `${url}?${queryString}`
|
|
29
|
+
}
|
|
30
|
+
return fetch(url, {
|
|
31
|
+
method: "GET",
|
|
32
|
+
headers: {
|
|
33
|
+
"X-CSRFToken": csrfToken,
|
|
34
|
+
Accept: "application/json",
|
|
35
|
+
"X-Requested-With": "XMLHttpRequest"
|
|
36
|
+
},
|
|
37
|
+
credentials: "include"
|
|
38
|
+
}).then(handleFetchErrors)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const post = (url, object = {}, files = {}) => {
|
|
42
|
+
const csrfToken = getCookie("csrftoken")
|
|
43
|
+
const fetchOptions = {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: {
|
|
46
|
+
"X-CSRFToken": csrfToken,
|
|
47
|
+
Accept: "application/json",
|
|
48
|
+
"X-Requested-With": "XMLHttpRequest"
|
|
49
|
+
},
|
|
50
|
+
credentials: "include"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (Object.keys(files).length) {
|
|
54
|
+
const body = new FormData()
|
|
55
|
+
body.append("csrfmiddlewaretoken", csrfToken)
|
|
56
|
+
body.append("json", JSON.stringify(object))
|
|
57
|
+
Object.keys(files).forEach(key => {
|
|
58
|
+
const value = files[key]
|
|
59
|
+
if (typeof value === "object" && value.file && value.filename) {
|
|
60
|
+
body.append(key, value.file, value.filename)
|
|
61
|
+
} else if (Array.isArray(value)) {
|
|
62
|
+
value.forEach(item => body.append(`${key}[]`, item))
|
|
63
|
+
} else {
|
|
64
|
+
body.append(key, value)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
fetchOptions.body = body
|
|
68
|
+
} else {
|
|
69
|
+
fetchOptions.headers["Content-Type"] = "application/json"
|
|
70
|
+
fetchOptions.body = JSON.stringify(object)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return fetch(url, fetchOptions).then(handleFetchErrors)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const postJson = (url, object = {}, files = {}) =>
|
|
77
|
+
post(url, object, files).then(response =>
|
|
78
|
+
response.json().then(json => ({json, status: response.status}))
|
|
79
|
+
)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const escapeText = text => {
|
|
2
|
+
if (typeof text !== "string") {
|
|
3
|
+
return String(text)
|
|
4
|
+
}
|
|
5
|
+
return text
|
|
6
|
+
.replace(/&/g, "&")
|
|
7
|
+
.replace(/</g, "<")
|
|
8
|
+
.replace(/>/g, ">")
|
|
9
|
+
.replace(/"/g, """)
|
|
10
|
+
.replace(
|
|
11
|
+
/[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]/g,
|
|
12
|
+
""
|
|
13
|
+
) // invalid in XML chars
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const unescapeText = text =>
|
|
17
|
+
text
|
|
18
|
+
.replace(/</g, "<")
|
|
19
|
+
.replace(/>/g, ">")
|
|
20
|
+
.replace(/"/g, '"')
|
|
21
|
+
.replace(/&/g, "&")
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Turn string literals into single line, removing spaces at start of line
|
|
25
|
+
*/
|
|
26
|
+
export const noSpaceTmp = (strings, ...values) => {
|
|
27
|
+
const tmpStrings = Array.from(strings)
|
|
28
|
+
|
|
29
|
+
let combined = ""
|
|
30
|
+
while (tmpStrings.length > 0 || values.length > 0) {
|
|
31
|
+
if (tmpStrings.length > 0) {
|
|
32
|
+
combined += tmpStrings.shift()
|
|
33
|
+
}
|
|
34
|
+
if (values.length > 0) {
|
|
35
|
+
combined += values.shift()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let out = ""
|
|
40
|
+
combined.split("\n").forEach(line => {
|
|
41
|
+
out += line.replace(/^\s*/g, "")
|
|
42
|
+
})
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2EE Encryptor - Handles encryption and decryption of document content
|
|
3
|
+
* for end-to-end encrypted documents.
|
|
4
|
+
*
|
|
5
|
+
* Uses AES-GCM (256-bit) for all encryption operations. Each encryption
|
|
6
|
+
* operation generates a random 12-byte IV (initialization vector) to
|
|
7
|
+
* ensure that encrypting the same plaintext twice produces different
|
|
8
|
+
* ciphertext.
|
|
9
|
+
*
|
|
10
|
+
* Encrypted data format (Appendix A of the E2EE plan):
|
|
11
|
+
* [IV (12 bytes)][Ciphertext (variable length)][Auth Tag (16 bytes, implicit in AES-GCM)]
|
|
12
|
+
*
|
|
13
|
+
* When stored as a string (e.g., in JSON fields), the entire structure
|
|
14
|
+
* is Base64-encoded.
|
|
15
|
+
*/
|
|
16
|
+
export class E2EEEncryptor {
|
|
17
|
+
/**
|
|
18
|
+
* Encrypt a string with AES-GCM.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} plaintext - The plaintext string to encrypt
|
|
21
|
+
* @param {CryptoKey} key - An AES-GCM key (from E2EEKeyManager.deriveKey)
|
|
22
|
+
* @returns {Promise<string>} Base64-encoded string (iv + ciphertext + auth tag)
|
|
23
|
+
*/
|
|
24
|
+
static async encrypt(plaintext, key) {
|
|
25
|
+
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
26
|
+
const encoded = new TextEncoder().encode(plaintext)
|
|
27
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
28
|
+
{name: "AES-GCM", iv: iv},
|
|
29
|
+
key,
|
|
30
|
+
encoded
|
|
31
|
+
)
|
|
32
|
+
// Prepend IV to ciphertext for storage
|
|
33
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength)
|
|
34
|
+
combined.set(iv, 0)
|
|
35
|
+
combined.set(new Uint8Array(ciphertext), iv.length)
|
|
36
|
+
return E2EEEncryptor._uint8ArrayToBase64(combined)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Decrypt a Base64-encoded AES-GCM ciphertext.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} ciphertextBase64 - Base64-encoded (iv + ciphertext + auth tag)
|
|
43
|
+
* @param {CryptoKey} key - An AES-GCM key (from E2EEKeyManager.deriveKey)
|
|
44
|
+
* @returns {Promise<string>} The decrypted plaintext string
|
|
45
|
+
*/
|
|
46
|
+
static async decrypt(ciphertextBase64, key) {
|
|
47
|
+
const combined = E2EEEncryptor._base64ToUint8Array(ciphertextBase64)
|
|
48
|
+
const iv = combined.slice(0, 12)
|
|
49
|
+
const ciphertext = combined.slice(12)
|
|
50
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
51
|
+
{name: "AES-GCM", iv: iv},
|
|
52
|
+
key,
|
|
53
|
+
ciphertext
|
|
54
|
+
)
|
|
55
|
+
return new TextDecoder().decode(decrypted)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Encrypt a JSON-serializable object.
|
|
60
|
+
*
|
|
61
|
+
* Serializes the object to JSON, then encrypts the JSON string.
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} obj - A JSON-serializable object
|
|
64
|
+
* @param {CryptoKey} key - An AES-GCM key
|
|
65
|
+
* @returns {Promise<string>} Base64-encoded encrypted data
|
|
66
|
+
*/
|
|
67
|
+
static encryptObject(obj, key) {
|
|
68
|
+
return E2EEEncryptor.encrypt(JSON.stringify(obj), key)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Decrypt to a JSON-serializable object.
|
|
73
|
+
*
|
|
74
|
+
* Decrypts the Base64-encoded ciphertext, then parses the result as JSON.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} ciphertextBase64 - Base64-encoded encrypted data
|
|
77
|
+
* @param {CryptoKey} key - An AES-GCM key
|
|
78
|
+
* @returns {Promise<Object>} The decrypted and parsed object
|
|
79
|
+
*/
|
|
80
|
+
static async decryptObject(ciphertextBase64, key) {
|
|
81
|
+
const plaintext = await E2EEEncryptor.decrypt(ciphertextBase64, key)
|
|
82
|
+
return JSON.parse(plaintext)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Encrypt an ArrayBuffer (for images and other binary data).
|
|
87
|
+
*
|
|
88
|
+
* @param {ArrayBuffer} buffer - The binary data to encrypt
|
|
89
|
+
* @param {CryptoKey} key - An AES-GCM key
|
|
90
|
+
* @returns {Promise<string>} Base64-encoded encrypted data (iv + ciphertext)
|
|
91
|
+
*/
|
|
92
|
+
static async encryptBuffer(buffer, key) {
|
|
93
|
+
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
94
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
95
|
+
{name: "AES-GCM", iv: iv},
|
|
96
|
+
key,
|
|
97
|
+
buffer
|
|
98
|
+
)
|
|
99
|
+
// Prepend IV to ciphertext
|
|
100
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength)
|
|
101
|
+
combined.set(iv, 0)
|
|
102
|
+
combined.set(new Uint8Array(ciphertext), iv.length)
|
|
103
|
+
return E2EEEncryptor._uint8ArrayToBase64(combined)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Decrypt to an ArrayBuffer.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} ciphertextBase64 - Base64-encoded encrypted data
|
|
110
|
+
* @param {CryptoKey} key - An AES-GCM key
|
|
111
|
+
* @returns {Promise<ArrayBuffer>} The decrypted binary data
|
|
112
|
+
*/
|
|
113
|
+
static decryptBuffer(ciphertextBase64, key) {
|
|
114
|
+
const combined = E2EEEncryptor._base64ToUint8Array(ciphertextBase64)
|
|
115
|
+
const iv = combined.slice(0, 12)
|
|
116
|
+
const ciphertext = combined.slice(12)
|
|
117
|
+
return crypto.subtle.decrypt({name: "AES-GCM", iv: iv}, key, ciphertext)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Encrypt an image File/Blob for upload.
|
|
122
|
+
*
|
|
123
|
+
* Reads the file as an ArrayBuffer, encrypts it, and returns
|
|
124
|
+
* a Blob with application/octet-stream type (since the encrypted
|
|
125
|
+
* data is opaque binary).
|
|
126
|
+
*
|
|
127
|
+
* @param {File|Blob} file - The image file to encrypt
|
|
128
|
+
* @param {CryptoKey} key - An AES-GCM key
|
|
129
|
+
* @returns {Promise<Blob>} An encrypted Blob with type application/octet-stream
|
|
130
|
+
*/
|
|
131
|
+
static async encryptImage(file, key) {
|
|
132
|
+
const buffer = await file.arrayBuffer()
|
|
133
|
+
const iv = crypto.getRandomValues(new Uint8Array(12))
|
|
134
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
135
|
+
{name: "AES-GCM", iv: iv},
|
|
136
|
+
key,
|
|
137
|
+
buffer
|
|
138
|
+
)
|
|
139
|
+
// Prepend IV to ciphertext
|
|
140
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength)
|
|
141
|
+
combined.set(iv, 0)
|
|
142
|
+
combined.set(new Uint8Array(ciphertext), iv.length)
|
|
143
|
+
return new Blob([combined], {type: "application/octet-stream"})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Decrypt an encrypted image back to an ArrayBuffer.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} ciphertextBase64 - Base64-encoded encrypted image data
|
|
150
|
+
* @param {CryptoKey} key - An AES-GCM key
|
|
151
|
+
* @returns {Promise<ArrayBuffer>} The decrypted image data
|
|
152
|
+
*/
|
|
153
|
+
static decryptImage(ciphertextBase64, key) {
|
|
154
|
+
return E2EEEncryptor.decryptBuffer(ciphertextBase64, key)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Decrypt a Base64-encoded ciphertext and return as a Base64 string.
|
|
159
|
+
*
|
|
160
|
+
* Useful for decrypting encrypted images that need to be stored
|
|
161
|
+
* as Base64 data URLs or re-exported.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} ciphertextBase64 - Base64-encoded encrypted data
|
|
164
|
+
* @param {CryptoKey} key - An AES-GCM key
|
|
165
|
+
* @returns {Promise<string>} Base64-encoded decrypted data
|
|
166
|
+
*/
|
|
167
|
+
static async decryptBufferToBase64(ciphertextBase64, key) {
|
|
168
|
+
const buffer = await E2EEEncryptor.decryptBuffer(ciphertextBase64, key)
|
|
169
|
+
const bytes = new Uint8Array(buffer)
|
|
170
|
+
let binary = ""
|
|
171
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
172
|
+
binary += String.fromCharCode(bytes[i])
|
|
173
|
+
}
|
|
174
|
+
return btoa(binary)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Decrypt an encrypted image and return a blob URL for display.
|
|
179
|
+
*
|
|
180
|
+
* Fetches the encrypted image file from the given URL, decrypts it,
|
|
181
|
+
* and creates a temporary object URL that can be used as an img src.
|
|
182
|
+
*
|
|
183
|
+
* @param {string} imageUrl - The URL of the encrypted image file
|
|
184
|
+
* @param {CryptoKey} key - An AES-GCM key
|
|
185
|
+
* @param {string} mimeType - The MIME type of the decrypted image
|
|
186
|
+
* @returns {Promise<string>} A blob URL for the decrypted image
|
|
187
|
+
*/
|
|
188
|
+
static async decryptImageToUrl(imageUrl, key, mimeType = "image/png") {
|
|
189
|
+
const response = await fetch(imageUrl)
|
|
190
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
191
|
+
const bytes = new Uint8Array(arrayBuffer)
|
|
192
|
+
let binary = ""
|
|
193
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
194
|
+
binary += String.fromCharCode(bytes[i])
|
|
195
|
+
}
|
|
196
|
+
const base64 = btoa(binary)
|
|
197
|
+
const decrypted = await E2EEEncryptor.decryptBuffer(base64, key)
|
|
198
|
+
const blob = new Blob([decrypted], {type: mimeType})
|
|
199
|
+
return URL.createObjectURL(blob)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- Private helper methods ---
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Convert a Uint8Array to a Base64-encoded string.
|
|
206
|
+
* @private
|
|
207
|
+
*/
|
|
208
|
+
static _uint8ArrayToBase64(bytes) {
|
|
209
|
+
let binary = ""
|
|
210
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
211
|
+
binary += String.fromCharCode(bytes[i])
|
|
212
|
+
}
|
|
213
|
+
return btoa(binary)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Convert a Base64-encoded string to a Uint8Array.
|
|
218
|
+
* @private
|
|
219
|
+
*/
|
|
220
|
+
static _base64ToUint8Array(base64) {
|
|
221
|
+
const binary = atob(base64)
|
|
222
|
+
const bytes = new Uint8Array(binary.length)
|
|
223
|
+
for (let i = 0; i < binary.length; i++) {
|
|
224
|
+
bytes[i] = binary.charCodeAt(i)
|
|
225
|
+
}
|
|
226
|
+
return bytes
|
|
227
|
+
}
|
|
228
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
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 DOCXExporterCitations {
|
|
9
|
+
constructor(docContent, settings, bibDB, csl, xml, origCitInfos = []) {
|
|
10
|
+
this.docContent = docContent
|
|
11
|
+
this.settings = settings
|
|
12
|
+
this.bibDB = bibDB
|
|
13
|
+
this.csl = csl
|
|
14
|
+
this.xml = xml
|
|
15
|
+
this.origCitInfos = origCitInfos
|
|
16
|
+
|
|
17
|
+
this.citInfos = []
|
|
18
|
+
this.citationTexts = []
|
|
19
|
+
this.pmCits = []
|
|
20
|
+
this.citFm = false
|
|
21
|
+
this.pmBib = false
|
|
22
|
+
this.styleXML = false
|
|
23
|
+
this.styleFilePath = "word/styles.xml"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
init() {
|
|
27
|
+
return this.xml
|
|
28
|
+
.getXml(this.styleFilePath)
|
|
29
|
+
.then(styleXML => {
|
|
30
|
+
this.styleXML = styleXML
|
|
31
|
+
return Promise.resolve()
|
|
32
|
+
})
|
|
33
|
+
.then(() => this.formatCitations())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Citations are highly interdependent -- so we need to format them all
|
|
37
|
+
// together before laying out the document.
|
|
38
|
+
formatCitations() {
|
|
39
|
+
if (this.origCitInfos.length) {
|
|
40
|
+
// Initial citInfos are taken from a previous run to include in bibliography,
|
|
41
|
+
// and they are removed before spitting out the citation entries for the given document.
|
|
42
|
+
// That way the bibliography should contain information from both.
|
|
43
|
+
this.citInfos = this.citInfos.concat(this.origCitInfos)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
descendantNodes(this.docContent).forEach(node => {
|
|
47
|
+
if (node.type === "citation") {
|
|
48
|
+
this.citInfos.push(JSON.parse(JSON.stringify(node.attrs)))
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
this.citFm = new FormatCitations(
|
|
52
|
+
this.csl,
|
|
53
|
+
this.citInfos,
|
|
54
|
+
this.settings.citationstyle,
|
|
55
|
+
"",
|
|
56
|
+
this.bibDB,
|
|
57
|
+
false,
|
|
58
|
+
this.settings.language
|
|
59
|
+
)
|
|
60
|
+
return this.citFm.init().then(() => {
|
|
61
|
+
this.citationTexts = this.citFm.citationTexts
|
|
62
|
+
if (this.origCitInfos.length) {
|
|
63
|
+
// Remove all citation texts originating from original starting citInfos
|
|
64
|
+
this.citationTexts.splice(0, this.origCitInfos.length)
|
|
65
|
+
}
|
|
66
|
+
this.convertCitations()
|
|
67
|
+
return Promise.resolve()
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
convertCitations() {
|
|
72
|
+
// There could be some formatting in the citations, so we parse them through the PM schema for final formatting.
|
|
73
|
+
// We need to put the citations each in a paragraph so that it works with
|
|
74
|
+
// the fiduswriter schema and so that the converter doesn't mash them together.
|
|
75
|
+
let citationsHTML = ""
|
|
76
|
+
this.citationTexts.forEach(ct => {
|
|
77
|
+
citationsHTML += `<p>${ct}</p>`
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (citationsHTML.length) {
|
|
81
|
+
// We create a standard body footnotecontainer node, add the citations into it, and parse it back.
|
|
82
|
+
const fnNode = fnSchema.nodeFromJSON({type: "footnotecontainer"})
|
|
83
|
+
|
|
84
|
+
const serializer = DOMSerializer.fromSchema(fnSchema)
|
|
85
|
+
const dom = serializer.serializeNode(fnNode)
|
|
86
|
+
dom.innerHTML = citationsHTML
|
|
87
|
+
this.pmCits = DOMParser.fromSchema(fnSchema)
|
|
88
|
+
.parse(dom, {topNode: fnNode})
|
|
89
|
+
.toJSON().content
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Now we do the same for the bibliography.
|
|
93
|
+
const cslBib = this.citFm.bibliography
|
|
94
|
+
if (cslBib && cslBib[1].length > 0) {
|
|
95
|
+
this.addReferenceStyle(cslBib[0])
|
|
96
|
+
const bibNode = cslBibSchema.nodeFromJSON({type: "cslbib"})
|
|
97
|
+
const cslSerializer = DOMSerializer.fromSchema(cslBibSchema)
|
|
98
|
+
const dom = cslSerializer.serializeNode(bibNode)
|
|
99
|
+
dom.innerHTML = cslBib[1].join("")
|
|
100
|
+
this.pmBib = DOMParser.fromSchema(cslBibSchema)
|
|
101
|
+
.parse(dom, {topNode: bibNode})
|
|
102
|
+
.toJSON()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
addReferenceStyle(bibInfo) {
|
|
107
|
+
const stylesEl = this.styleXML.query("w:styles")
|
|
108
|
+
if (
|
|
109
|
+
!this.styleXML.query("w:style", {
|
|
110
|
+
"w:styleId": "BibliographyHeading"
|
|
111
|
+
})
|
|
112
|
+
) {
|
|
113
|
+
// There is no style definition for the bibliography heading. We have to add it.
|
|
114
|
+
const headingStyleDef = `
|
|
115
|
+
<w:style w:type="paragraph" w:styleId="BibliographyHeading">
|
|
116
|
+
<w:name w:val="Bibliography Heading"/>
|
|
117
|
+
<w:basedOn w:val="Heading"/>
|
|
118
|
+
<w:pPr>
|
|
119
|
+
<w:suppressLineNumbers/>
|
|
120
|
+
<w:ind w:left="0" w:hanging="0"/>
|
|
121
|
+
</w:pPr>
|
|
122
|
+
<w:rPr>
|
|
123
|
+
<w:b/>
|
|
124
|
+
<w:bCs/>
|
|
125
|
+
<w:sz w:val="32"/>
|
|
126
|
+
<w:szCs w:val="32"/>
|
|
127
|
+
</w:rPr>
|
|
128
|
+
</w:style>`
|
|
129
|
+
stylesEl.appendXML(headingStyleDef)
|
|
130
|
+
}
|
|
131
|
+
// The style called "Bibliography" will override any previous style
|
|
132
|
+
// of the same name.
|
|
133
|
+
const stylesParStyle = this.styleXML.query("w:style", {
|
|
134
|
+
"w:styleId": "Bibliography"
|
|
135
|
+
})
|
|
136
|
+
if (stylesParStyle) {
|
|
137
|
+
stylesParStyle.parentElement.removeChild(stylesParStyle)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const lineHeight = 240 * bibInfo.linespacing
|
|
141
|
+
const marginBottom = 240 * bibInfo.entryspacing
|
|
142
|
+
let marginLeft = 0,
|
|
143
|
+
hangingIndent = 0,
|
|
144
|
+
tabStops = ""
|
|
145
|
+
|
|
146
|
+
if (bibInfo.hangingindent) {
|
|
147
|
+
marginLeft = 720
|
|
148
|
+
hangingIndent = 720
|
|
149
|
+
} else if (bibInfo["second-field-align"]) {
|
|
150
|
+
// We calculate 120 as roughly equivalent to one letter width.
|
|
151
|
+
const firstFieldWidth = (bibInfo.maxoffset + 1) * 120
|
|
152
|
+
if (bibInfo["second-field-align"] === "margin") {
|
|
153
|
+
hangingIndent = firstFieldWidth
|
|
154
|
+
tabStops =
|
|
155
|
+
'<w:tabs><w:tab w:val="left" w:pos="0" w:leader="none"/></w:tabs>'
|
|
156
|
+
} else {
|
|
157
|
+
hangingIndent = firstFieldWidth
|
|
158
|
+
marginLeft = firstFieldWidth
|
|
159
|
+
tabStops = `<w:tabs><w:tab w:val="left" w:pos="${firstFieldWidth}" w:leader="none"/></w:tabs>`
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const styleDef = `
|
|
163
|
+
<w:style w:type="paragraph" w:styleId="Bibliography">
|
|
164
|
+
<w:name w:val="Bibliography"/>
|
|
165
|
+
<w:basedOn w:val="Normal"/>
|
|
166
|
+
<w:qFormat/>
|
|
167
|
+
<w:pPr>
|
|
168
|
+
${tabStops}
|
|
169
|
+
<w:spacing w:lineRule="atLeast" w:line="${lineHeight}" w:before="0" w:after="${marginBottom}"/>
|
|
170
|
+
<w:ind w:left="${marginLeft}" w:hanging="${hangingIndent}"/>
|
|
171
|
+
</w:pPr>
|
|
172
|
+
<w:rPr></w:rPr>
|
|
173
|
+
</w:style>`
|
|
174
|
+
|
|
175
|
+
stylesEl.appendXML(styleDef)
|
|
176
|
+
}
|
|
177
|
+
}
|