/
export.js
187 lines (175 loc) · 7.78 KB
/
export.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
/**
* This module installs into the editor a menu item for exporting Lurch document
* content as LaTeX, which makes it handy for pasting into a LaTeX editor, such
* as Overleaf.
*
* @module Export
*/
import { Atom } from './atoms.js'
import { Shell } from './shells.js'
import { Dialog, LongTextInputItem, CheckBoxItem } from './dialog.js'
import { escapeLatex } from './utilities.js'
import { appSettings } from './settings-install.js'
// Internal use only
// The preamble that will be prefixed to any LaTeX document created by this
// module, if the user chooses to wrap the result in a document environment
const latexPreamble = `\\documentclass{article}
\\usepackage{amsmath}
\\usepackage{amssymb}
\\usepackage{amsthm}
\\usepackage{url}
\\newtheorem*{theorem}{Theorem}
\\newtheorem*{lemma}{Lemma}
\\newtheorem*{corollary}{Corollary}
\\newtheorem*{lurchrule}{Rule}
\\newtheorem*{axiom}{Axiom}
\\newtheorem*{definition}{Definition}
\\newtheorem*{recall}{Recall}
\\title{Lurch Document}
\\author{Lurch User}
\\date{\\today}
\\begin{document}
\\maketitle
`
// The corresponding ending text for the preamble above
const latexPostamble = `
\\end{document}`
// Internal use only
// Convert an HTML node (which can contain an entire document) to LaTeX,
// recursively
const htmlNodeToLatex = ( node, editor ) => {
// Utility function for recurring on all children and joining their results
const recur = () => Array.from( node.childNodes ).map( child =>
htmlNodeToLatex( child, editor ) ).join( '' )
// If it's an atom or shell, defer to that class's conversion method
if ( Atom.isAtomElement( node ) ) {
const atom = Atom.from( node, editor )
appSettings.load()
return !( atom instanceof Shell ) ? atom.toLatex()
: appSettings.get( 'export LaTeX shells' ) ? atom.toLatex( recur() )
: recur()
}
// If it's a text node, just use its (escaped and cleaned) contents
if ( !node.tagName )
return escapeLatex( node.textContent.replace( '\u00a0', ' ' ) ) // nbsp
// If it's been centered, wrap it in a LaTeX center environment
if ( node.style.textAlign == 'center' ) {
node.style.textAlign = 'left'
const withoutCentering = htmlNodeToLatex( node, editor )
return `\n\\begin{center}\n${withoutCentering}\n\\end{center}\n`
}
// Handle the most common HTML tags that might show up in a Lurch document
switch ( node.tagName.toLowerCase() ) {
case 'br' : return '\n\\hfill\n\n'
case 'hr' : return '\n\\hfill\n\\hrule\n\\hfill\n'
case 'p' : return `\n\n${recur()}\n\n`
case 'a' : return `\\href{${node.href}}{${recur()}}`
case 'strong' : return `\\textbf{${recur()}}`
case 'em' : return `\\textit{${recur()}}`
case 'sup' : return `\\textsuperscript{${recur()}}`
case 'sub' : return `\\textsubscript{${recur()}}`
case 'code' : return `\\texttt{${recur()}}`
case 'blockquote' : return `\\begin{quote}\n${recur()}\n\\end{quote}`
case 'span' : return node.style.textDecoration == 'underline' ?
`\\underline{${recur()}}` : recur()
case 'ol' : return `\\begin{enumerate}\n${recur()}\n\\end{enumerate}`
case 'ul' : return `\\begin{itemize}\n${recur()}\n\\end{itemize}`
case 'li' : return `\\item ${recur()}`
case 'h1' : return `\\section*{${recur()}}`
case 'h2' : return `\\subsection*{${recur()}}`
case 'h3' : return `\\subsubsection*{${recur()}}`
case 'h4' :
case 'h5' :
case 'h6' : return `\\textbf{${recur()}}`
default : return recur()
}
}
// Internal use only
// The LaTeX code that comes out of the above function sometimes has some
// inelegant content, and this function smooths it out, as described below
const cleanUpLatex = latex => {
// Trim each line and reduce many blank lines to just one
latex = latex.replace( /[^\S\r\n]+\n/g, '\n' )
.replace( /\n[^\S\r\n]+/g, '\n' )
.replace( /\n\n\n+/g, '\n\n' )
// A centering around an align is redundant
latex = latex.replace( /\\begin{center}\n?\n?\\begin{align}/g, '\\begin{align}' )
.replace( /\\end{align}\n?\n?\\end{center}/g, '\\end{align}' )
// Done
return latex
}
/**
* This function should be called in the editor's setup routine. It installs
* one feature into the editor:
*
* * a menu item for exporting the current document (or just the selection in
* it) as LaTeX, for pasting into a LaTeX editor, such as Overleaf
*
* @param {tinymce.Editor} editor - the editor in which to install the feature
* described above
* @function
*/
export const install = editor => {
editor.ui.registry.addMenuItem( 'exportlatex', {
icon : 'new-tab',
text : 'Export as LaTeX...',
tooltip : 'Export document or selection in LaTeX format',
onAction : () => {
// Compute the LaTeX versions of the whole document and of just the
// user's current selection
const offScreenDiv = document.createElement( 'div' )
offScreenDiv.innerHTML = editor.getContent()
const fullLatex = cleanUpLatex(
htmlNodeToLatex( offScreenDiv, editor ) )
offScreenDiv.innerHTML = editor.selection.getContent()
const selectedLatex = cleanUpLatex(
htmlNodeToLatex( offScreenDiv, editor ) )
// Create a function that can compute the text to copy to the
// clipboard, based on the above data, plus options the user will
// specify via checkboxes in the dialog
const latexToCopy = ( selectedOnly, addWrapper ) => {
if ( selectedOnly && /^\s*$/.exec( selectedLatex ) )
return '(no text selected in document)'
return ( addWrapper ? latexPreamble : '' )
+ ( selectedOnly ? selectedLatex : fullLatex )
+ ( addWrapper ? latexPostamble : '' )
}
// Create the dialog
const dialog = new Dialog( 'Export as LaTeX', editor )
dialog.json.size = 'medium'
dialog.addItem( new CheckBoxItem(
'selected', 'Convert only the selection' ) )
dialog.addItem( new CheckBoxItem(
'wrapper', 'Wrap the result in a document environment' ) )
dialog.addItem( new LongTextInputItem( 'code',
'Use the following code in your LaTeX document' ) )
appSettings.load()
dialog.setInitialData( {
selected : appSettings.get( 'export LaTeX selection only' ),
wrapper : appSettings.get( 'add LaTeX document wrapper' ),
code : '' // see below
} )
dialog.removeButton( 'Cancel' )
dialog.setDefaultFocus( 'code' )
dialog.show()
// Store the essential elements from the dialog in local variables
const textarea = dialog.querySelector( 'textarea' )
const checkBoxes = dialog.querySelectorAll( 'input[type="checkbox"]' )
// Create an event handler that can populate the textarea with the
// text to copy, highlight it, and scroll to the top
const updateTextarea = () => setTimeout( () => {
textarea.value = latexToCopy(
checkBoxes[0].checked, checkBoxes[1].checked )
textarea.select()
textarea.setAttribute( 'readonly', 'true' )
textarea.setAttribute( 'rows', 15 )
textarea.scrollTo( 0, 0 )
} )
// Update the text now, plus whenever the user changes options
updateTextarea()
checkBoxes.forEach( checkBox =>
checkBox.addEventListener( 'change', updateTextarea ) )
}
} )
}
export default { install }