Lurch web app user interface

source

export.js


/**
 * 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 }