forked from vincentdoerig/latex-css
-
Notifications
You must be signed in to change notification settings - Fork 0
/
startup.js
349 lines (311 loc) · 12.5 KB
/
startup.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
/**
* Add promise when Mathjax is found
*/
const mathjaxStartup = function() {
let MathJax = window.MathJax || {};
if (typeof MathJax.typeset !== "undefined") {
console.log("LatexCss: MathJax script found");
return MathJax.startup.promise;
}
}
/**
* Add promise when Prism.js is found
*/
const prismHighlight = function() {
let Prism = window.Prism || {};
if (typeof Prism.highlight !== "undefined") {
console.log("LatexCss: Prism script found");
// Variable to save the `resolve` from Prism.js Promise.
let prismHighlightResolve;
// Verify when Prism has finished to highlight all elements.
const numCodeItems = document.querySelectorAll("[class*=language-]").length;
let numCodeItemsProcessed = 0;
// Promise with a deferred `resolve` trick.
const prismHighlightPromise = new Promise(function(resolve) {
prismHighlightResolve = resolve;
});
// Prism runs a hook for every single element highlighted
Prism.hooks.add('complete', function(env) {
numCodeItemsProcessed++;
if (numCodeItemsProcessed == numCodeItems) {
prismHighlightResolve(numCodeItemsProcessed);
};
});
return prismHighlightPromise;
};
}
/**
* Remove `loading` attribute from `img` elements
*/
const removeImageLoading = function() {
const removeImgLoadingPromise = new Promise(function(resolve) {
const ImgElements = document.querySelectorAll("img");
resolve(ImgElements.forEach((element) => element.removeAttribute("loading")));
});
return removeImgLoadingPromise;
}
/**
* Add ToC page numbers if `toc-page-numbers` class is used in `body` (adding a
* "container" to insert ToC dotted line)
*/
const addTocNumers = function() {
const bodyClasses = document.body.classList;
if (bodyClasses.contains("toc-page-numbers")) {
console.log("LatexCss: add page numbers to ToC")
const addTocField = function (element){
// Enclose ToC item content inside a `span` element and add other `span`
// container to insert the dotted line; both of them inside a `div`.
const tocItemContent = document.createElement("span");
tocItemContent.classList.add("toc-item-content");
element.childNodes.forEach(function(el) {
tocItemContent.append(el);
});
const tocField = document.createElement("span");
tocField.classList.add("dotted-line");
const tocItemContainer = document.createElement("div");
tocItemContainer.classList.add("toc-item-container");
tocItemContainer.append(tocItemContent, tocField);
element.textContent = "";
element.append(tocItemContainer);
}
const addTocNumbersPromise = new Promise(function(resolve) {
const tocItems = document.querySelectorAll("nav li > a");
resolve(tocItems.forEach(addTocField));
});
return addTocNumbersPromise;
}
}
/*
* Footnotes:
* It's going to be supposed that each one of the footnotes text is a children
* of the `div#footnotes` container, and it has an `id` attribute. And the
* identification is done by the coincidence of the `id` in footnote with the
* `href` in some link inside `sup` element, obtaining the footnote text and
* the footnote mark.
* If there is a remaining HTML content inside `div#footnotes`, not identified
* with any footnote mark, this content will be ignored/removed.
*/
/**
* Get footnotes text from `div#footnotes` element in document.
* @returns {Object.<string, HTMLElement>} Each keys is a footnote `id`, each value is the corresponding footnote content.
*/
const getFootnotesText = function() {
const fnContentAll = document.querySelectorAll("div.footnotes>[id]");
const footnotesTextObj = {};
fnContentAll.forEach((el) => footnotesTextObj[el.id] = el);
return footnotesTextObj;
}
/**
* Get the corresponding document footnotes marks (the full link) for each footnote in footnotes `Div`, or `null`.
* @param {Array.<string>} footnotesTextKeys - An array with the footnotes id's.
* @returns {Object.<string, HTMLElement|null>} Each key is the corresponding footnote content id, the value is the footnote mark link.
*/
const getFootnotesMark = function(footnotesTextKeys) {
const footnotesMarkObj = {};
footnotesTextKeys.forEach((key) => {
const footnoteMarkLink =
document.querySelector("sup a[href=\"#" + key + "\"]");
footnotesMarkObj[key] = footnoteMarkLink;
});
return footnotesMarkObj;
}
/**
* Remove the manual marker from footnote text content.
* @param {HTMLElement} footnoteContent - The content of the footnote.
* @param {string} footnoteMarkContent - Text used as footnote mark.
* @returns {boolean|Error} `true`/`false` depending on whether the footnote content is processed, and error if an invalid character has been used in footnote mark.
*/
const cleanFootnoteText = function(footnoteContent, footnoteMarkContent) {
const START_CHARS = ["\\(", "\\[", "<", "{"].join("");
const SEP_CHARS = [".", "-", ">", "}", "\\s", "\\)", "\\]"].join("");
const CLEAN_CHARS =
[["(", "\\("], [")", "\\)"], ["[", "\\["], ["]", "\\]"],
["*", "\\*"]];
CLEAN_CHARS.forEach((el) => {
footnoteMarkContent = footnoteMarkContent.replaceAll(el[0], el[1]);
});
// Only one match must happen. Consecutive white spaces are joined into one
// later, so a simpler regex can be used. The regexp tries to capture the
// text content in footnote mark until (included) an non-separator character.
const regExpStr =
"(\\s?[" + START_CHARS + "]?\\s?" + footnoteMarkContent + "\\s?[" +
SEP_CHARS + "]?\\s?)[^" + SEP_CHARS + "]+?";
const footnoteId = footnoteContent.id;
let regExpMark;
try {
regExpMark = new RegExp(regExpStr);
} catch (err) {
const errInvalidChar = new Error("Invalid character used in footnote mark #"
+ footnoteContent.id + "!");
errInvalidChar.name = "InvalidMarkCharacter";
errInvalidChar.cause = err;
throw errInvalidChar;
}
const treeWalker =
document.createTreeWalker(footnoteContent, NodeFilter.SHOW_ALL);
let currentNode = treeWalker.currentNode;
const removeTextNodes = []; // Text nodes marked to be removed
const walkedNonTextNodes = []; // Non-text nodes marked to be removed
let footnoteMarkInText = false;
let currentText = ""; // Aggregated text from the walked nodes
while(currentNode) {
let prevText = currentText;
if (currentNode.nodeType === Node.TEXT_NODE) {
// Remove line breaks and multiple white spaces to simplify `regExpMark`
// Regexp for different OS line breaks: /(\r\n|\n|\r)/gm
currentText =
currentText + currentNode.nodeValue.replace(/(\r\n|\n|\r)/gm, "");
currentText = currentText.replace(/\s+/gm, " ");
if (regExpMark.test(currentText.trimStart())) {
// If the aggregated text matches "footnote-mark"+"nonsep-character",
// then clean the footnote mark text inside this last element.
const textMatch = currentText.match(regExpMark);
const textMarkPieceToRemove = textMatch[1].replace(prevText.trimStart(), "");
currentNode.nodeValue =
currentNode.nodeValue.replace(textMarkPieceToRemove, "").trimStart();
footnoteMarkInText = true;
break;
} else {
// If the aggregated text does not match the footnote mark, this last
// text node is marked to be deleted, if there is finally a
// coincidence.
removeTextNodes.push(currentNode);
currentNode = treeWalker.nextNode();
}
} else {
// When it's not a text node (and there is no match for footnote mark
// yet), this node is marked to be deleted, if footnote mark is finally
// found in text.
walkedNonTextNodes.push(currentNode);
currentNode = treeWalker.nextNode();
}
} // while
// Process (clean) footnote content, if footnote mark has been found in
// footnote content
if (footnoteMarkInText) {
removeTextNodes.reverse().forEach((el) => {
el.remove();
});
walkedNonTextNodes.reverse().forEach((el) => {
if (el.childNodes.length === 0) {
el.remove();
}
});
return true;
} else {
console.warn("LatexCSS: footnote mark not found in footnote content for `"
+ footnoteId + "`");
return false;
}
}
/**
* Transform LaTeX.css footnotes to CSS Paged Media footnotes.
* @returns {HTMLElement} Unmatched footnotes with their corresponding marks.
*/
const processFootnotes = function() {
const footnotesDiv = document.querySelector("div.footnotes");
if (!footnotesDiv) return;
const footnoteTextAll = getFootnotesText();
const footnoteTextKeyAll = Object.keys(footnoteTextAll);
const footnoteMarkAll = getFootnotesMark(footnoteTextKeyAll);
footnoteTextKeyAll.forEach((key) => {
if (footnoteMarkAll[key] !== null) {
const footnoteText = footnoteTextAll[key];
const footnoteMark = footnoteMarkAll[key].parentElement;
// Remove footnote mark from footnote text content
try {
cleanFootnoteText(footnoteText, footnoteMarkAll[key].textContent);
} catch (err) {
console.error(err);
console.warn("LatexCss: footnotetext #" + key + " cannot be processed");
}
// Build the CSS Paged Media footnote
const pagedMediaFootnote = document.createElement("span");
pagedMediaFootnote.classList.add("footnote");
footnoteText.childNodes.forEach((el) => {
// footnotesDiv will be finally removed (=> no duplicate ID's)
pagedMediaFootnote.append(el.cloneNode(true));
});
// Footnote mark wrapper to preserve the original footnote mark `id`
const pagedMediaFootnoteWrapper = document.createElement("span");
if (footnoteMarkAll[key].id) {
pagedMediaFootnoteWrapper.id = footnoteMarkAll[key].id;
}
pagedMediaFootnoteWrapper.append(pagedMediaFootnote);
// Replace footnotemark with CSS Paged Media footnote
footnoteMark.replaceWith(pagedMediaFootnoteWrapper);
} else {
console.warn("LatexCss: The footnote with id", key,
"does not have a matching footnote mark, and it will be ignored.");
}
});
// Remove footnotes `div` element
const footnotesDivRemoved = footnotesDiv.cloneNode(true);
footnotesDiv.remove();
return footnotesDivRemoved;
}
/**
* Process LaTeX.css footnotes.
*/
const processFootnotesStartup = function() {
return new Promise (function(resolve) {
const footnotesRemoved = processFootnotes();
resolve(footnotesRemoved);
});
}
/**
* Transform sidenotes from one or both margins to Paged Media footnotes,
* depending on the the sidenotes style choosen: `sidenotes-inner`,
* `sidenotes-outer` or none of them.
*/
const sideToFoot = function() {
const sideToFootPromise = new Promise(function(resolve) {
const bodyElClasses = document.body.classList;
let sidenoteList;
if (bodyElClasses.contains("sidenotes-inner")) {
sidenoteList = document.querySelectorAll("label + input + span.sidenote:not(.left)");
} else if (bodyElClasses.contains("sidenotes-outer")) {
sidenoteList = document.querySelectorAll("label + input + span.sidenote.left");
} else {
sidenoteList = document.querySelectorAll("label + input + span.sidenote");
}
if (sidenoteList.length !== 0) {
sidenoteList.forEach((sidenote) => {
const snInput = sidenote.previousElementSibling;
const snLabel = snInput.previousElementSibling;
// Build the CSS Paged Media footnote
const pagedMediaFootnote = document.createElement("span");
pagedMediaFootnote.classList.add("footnote");
sidenote.childNodes.forEach((child) => {
// `sidenote` will be finally removed (no duplicate ID's)
pagedMediaFootnote.append(child.cloneNode(true));
});
sidenote.replaceWith(pagedMediaFootnote);
snInput.remove();
snLabel.remove();
});
resolve(true);
} else {
resolve(false);
}
});
return sideToFootPromise;
}
const PROMS_FUNCS = [mathjaxStartup, prismHighlight, removeImageLoading, addTocNumers,
processFootnotesStartup, sideToFoot];
/**
* Run all promise functions to build the list of promises that will be applied
* @returns Single promise constructed with `Promise.all` from the list of
* promises applied.
*/
const startupPromise = function() {
const onLoadPromises = [];
PROMS_FUNCS.forEach(fun => {
let currentPromise = fun();
if (typeof(currentPromise) !== 'undefined') {
onLoadPromises.push(currentPromise);
}
});
return Promise.all(onLoadPromises);
}
export { startupPromise };