| </Match> | ||
| - <Match class="com.keenwrite.processors.HtmlPreviewProcessor"> | ||
| + <Match class="com.keenwrite.processors.html.HtmlPreviewProcessor"> | ||
| <Method name="<init>" /> | ||
| <Bug code="ST" /> |
| import com.keenwrite.processors.Processor; | ||
| import com.keenwrite.processors.ProcessorContext; | ||
| -import com.keenwrite.processors.RBootstrapProcessor; | ||
| +import com.keenwrite.processors.r.RBootstrapProcessor; | ||
| import java.io.IOException; |
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| package com.keenwrite; | ||
| * For XHTML exports, encode TeX using {@code $} delimiters. | ||
| */ | ||
| - XHTML_TEX( ".xml" ), | ||
| + XHTML_TEX( ".xhtml" ), | ||
| + | ||
| + /** | ||
| + * For TEXT exports, encode TeX using {@code $} delimiters. | ||
| + */ | ||
| + TEXT_TEX( ".txt" ), | ||
| /** | ||
| import com.keenwrite.preferences.Workspace; | ||
| import com.keenwrite.preview.HtmlPreview; | ||
| -import com.keenwrite.processors.HtmlPreviewProcessor; | ||
| +import com.keenwrite.processors.html.HtmlPreviewProcessor; | ||
| import com.keenwrite.processors.Processor; | ||
| import com.keenwrite.processors.ProcessorContext; | ||
| import static com.keenwrite.io.SysFile.toFile; | ||
| import static com.keenwrite.preferences.AppKeys.*; | ||
| -import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | ||
| +import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | ||
| import static com.keenwrite.processors.ProcessorContext.Mutator; | ||
| import static com.keenwrite.processors.ProcessorContext.builder; | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors; | ||
| - | ||
| -import com.keenwrite.preview.HtmlPreview; | ||
| - | ||
| -/** | ||
| - * Responsible for notifying the {@link HtmlPreview} when the succession | ||
| - * chain has updated. This decouples knowledge of changes to the editor panel | ||
| - * from the HTML preview panel as well as any processing that takes place | ||
| - * before the final HTML preview is rendered. This is the last link in the | ||
| - * processor chain. | ||
| - */ | ||
| -public final class HtmlPreviewProcessor extends ExecutorProcessor<String> { | ||
| - /** | ||
| - * There is only one preview panel. | ||
| - */ | ||
| - private static HtmlPreview sHtmlPreview; | ||
| - | ||
| - /** | ||
| - * Constructs the end of a processing chain. | ||
| - * | ||
| - * @param htmlPreview The pane to update with the post-processed document. | ||
| - */ | ||
| - public HtmlPreviewProcessor( final HtmlPreview htmlPreview ) { | ||
| - sHtmlPreview = htmlPreview; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Update the preview panel using HTML from the succession chain. | ||
| - * | ||
| - * @param html The document content to render in the preview pane. The HTML | ||
| - * should not contain a doctype, head, or body tag. | ||
| - * @return The given {@code html} string. | ||
| - */ | ||
| - @Override | ||
| - public String apply( final String html ) { | ||
| - assert html != null; | ||
| - | ||
| - sHtmlPreview.render( html ); | ||
| - return html; | ||
| - } | ||
| -} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors; | ||
| - | ||
| -/** | ||
| - * Responsible for transforming a string into itself. This is used at the | ||
| - * end of a processing chain when no more processing is required. | ||
| - */ | ||
| -public final class IdentityProcessor extends ExecutorProcessor<String> { | ||
| - public static final IdentityProcessor IDENTITY = new IdentityProcessor(); | ||
| - | ||
| - /** | ||
| - * Constructs a new instance having no successor (the default successor is | ||
| - * {@code null}). | ||
| - */ | ||
| - private IdentityProcessor() { | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the given string without modification. | ||
| - * | ||
| - * @param s The string to return. | ||
| - * @return The value of s. | ||
| - */ | ||
| - @Override | ||
| - public String apply( final String s ) { | ||
| - return s; | ||
| - } | ||
| -} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors; | ||
| - | ||
| -import com.keenwrite.typesetting.Typesetter; | ||
| - | ||
| -import static com.keenwrite.Bootstrap.APP_TITLE_ABBR; | ||
| -import static com.keenwrite.events.StatusEvent.clue; | ||
| -import static com.keenwrite.io.MediaType.TEXT_XML; | ||
| -import static com.keenwrite.io.SysFile.normalize; | ||
| -import static com.keenwrite.typesetting.Typesetter.Mutator; | ||
| -import static com.keenwrite.util.Strings.sanitize; | ||
| -import static java.nio.charset.StandardCharsets.UTF_8; | ||
| -import static java.nio.file.Files.deleteIfExists; | ||
| -import static java.nio.file.Files.writeString; | ||
| - | ||
| -/** | ||
| - * Responsible for using a typesetting engine to convert an XHTML document | ||
| - * into a PDF file. This must not be run from the JavaFX thread. | ||
| - */ | ||
| -public final class PdfProcessor extends ExecutorProcessor<String> { | ||
| - private final ProcessorContext mProcessorContext; | ||
| - | ||
| - public PdfProcessor( final ProcessorContext context ) { | ||
| - assert context != null; | ||
| - mProcessorContext = context; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Converts a document by calling a third-party application to typeset the | ||
| - * given XHTML document. | ||
| - * | ||
| - * @param xhtml The document to convert to a PDF file. | ||
| - * @return {@code null} because there is no valid return value from generating | ||
| - * a PDF file. | ||
| - */ | ||
| - public String apply( final String xhtml ) { | ||
| - try { | ||
| - clue( "Main.status.typeset.create" ); | ||
| - | ||
| - final var context = mProcessorContext; | ||
| - final var targetPath = context.getTargetPath(); | ||
| - clue( "Main.status.typeset.setting", "target", targetPath ); | ||
| - | ||
| - final var parent = normalize( targetPath.toAbsolutePath().getParent() ); | ||
| - | ||
| - final var document = TEXT_XML.createTempFile( APP_TITLE_ABBR, parent ); | ||
| - final var sourcePath = writeString( document, xhtml, UTF_8 ); | ||
| - clue( "Main.status.typeset.setting", "source", sourcePath ); | ||
| - | ||
| - final var themeDir = normalize( context.getThemeDir() ); | ||
| - clue( "Main.status.typeset.setting", "themes", themeDir ); | ||
| - | ||
| - final var imageDir = normalize( context.getImageDir() ); | ||
| - clue( "Main.status.typeset.setting", "images", imageDir ); | ||
| - | ||
| - final var imageOrder = context.getImageOrder(); | ||
| - clue( "Main.status.typeset.setting", "order", imageOrder ); | ||
| - | ||
| - final var cacheDir = normalize( context.getCacheDir() ); | ||
| - clue( "Main.status.typeset.setting", "caches", cacheDir ); | ||
| - | ||
| - final var fontDir = normalize( context.getFontDir() ); | ||
| - clue( "Main.status.typeset.setting", "fonts", fontDir ); | ||
| - | ||
| - final var rWorkDir = normalize( context.getRWorkingDir() ); | ||
| - clue( "Main.status.typeset.setting", "r-work", rWorkDir ); | ||
| - | ||
| - final var modesEnabled = sanitize( context.getModesEnabled() ); | ||
| - clue( "Main.status.typeset.setting", "mode", modesEnabled ); | ||
| - | ||
| - final var autoRemove = context.getAutoRemove(); | ||
| - clue( "Main.status.typeset.setting", "purge", autoRemove ); | ||
| - | ||
| - final var typesetter = Typesetter | ||
| - .builder() | ||
| - .with( Mutator::setTargetPath, targetPath ) | ||
| - .with( Mutator::setSourcePath, sourcePath ) | ||
| - .with( Mutator::setThemeDir, themeDir ) | ||
| - .with( Mutator::setImageDir, imageDir ) | ||
| - .with( Mutator::setCacheDir, cacheDir ) | ||
| - .with( Mutator::setFontDir, fontDir ) | ||
| - .with( Mutator::setModesEnabled, modesEnabled ) | ||
| - .with( Mutator::setAutoRemove, autoRemove ) | ||
| - .build(); | ||
| - | ||
| - try { | ||
| - typesetter.typeset(); | ||
| - } | ||
| - finally { | ||
| - // Smote the temporary file after typesetting the document. | ||
| - if( typesetter.autoRemove() ) { | ||
| - deleteIfExists( document ); | ||
| - } | ||
| - } | ||
| - } catch( final Exception ex ) { | ||
| - // Typesetter runtime exceptions will pass up the call stack. | ||
| - clue( "Main.status.typeset.failed", ex ); | ||
| - } | ||
| - | ||
| - // Do not continue processing (the document was typeset into a binary). | ||
| - return null; | ||
| - } | ||
| -} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors; | ||
| - | ||
| -/** | ||
| - * This is the default processor used when an unknown file name extension is | ||
| - * encountered. It processes the text by enclosing it in an HTML {@code <pre>} | ||
| - * element. | ||
| - */ | ||
| -public final class PreformattedProcessor extends ExecutorProcessor<String> { | ||
| - | ||
| - /** | ||
| - * Passes the link to the super constructor. | ||
| - * | ||
| - * @param successor The next processor in the chain to use for text | ||
| - * processing. | ||
| - */ | ||
| - public PreformattedProcessor( final Processor<String> successor ) { | ||
| - super( successor ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the given string, modified with "pre" tags. | ||
| - * | ||
| - * @param t The string to return, enclosed in "pre" tags. | ||
| - * @return The value of t wrapped in "pre" tags. | ||
| - */ | ||
| - @Override | ||
| - public String apply( final String t ) { | ||
| - return "<pre>" + t + "</pre>"; | ||
| - } | ||
| -} | ||
| import com.keenwrite.io.MediaType; | ||
| import com.keenwrite.io.MediaTypeExtension; | ||
| +import com.keenwrite.processors.variable.VariableProcessor; | ||
| import com.keenwrite.sigils.PropertyKeyOperator; | ||
| import com.keenwrite.sigils.SigilKeyOperator; | ||
| import static com.keenwrite.io.SysFile.toFile; | ||
| import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate; | ||
| -import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | ||
| +import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | ||
| import static com.keenwrite.processors.text.TextReplacementFactory.replace; | ||
| import static com.keenwrite.util.Strings.sanitize; | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +/* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| package com.keenwrite.processors; | ||
| +import com.keenwrite.processors.html.PreformattedProcessor; | ||
| +import com.keenwrite.processors.html.XhtmlProcessor; | ||
| import com.keenwrite.processors.markdown.MarkdownProcessor; | ||
| +import com.keenwrite.processors.pdf.PdfProcessor; | ||
| +import com.keenwrite.processors.text.TextProcessor; | ||
| +import com.keenwrite.processors.variable.VariableProcessor; | ||
| +import static com.keenwrite.ExportFormat.TEXT_TEX; | ||
| import static com.keenwrite.io.FileType.RMARKDOWN; | ||
| import static com.keenwrite.io.FileType.SOURCE; | ||
| -import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | ||
| +import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | ||
| /** | ||
| case NONE -> preview; | ||
| case XHTML_TEX -> createXhtmlProcessor( context ); | ||
| + case TEXT_TEX -> createTextProcessor( context ); | ||
| case APPLICATION_PDF -> createPdfProcessor( context ); | ||
| default -> createIdentityProcessor( context ); | ||
| }; | ||
| final var inputType = context.getSourceFileType(); | ||
| final Processor<String> processor; | ||
| - // When there's no preview, convert to HTML. | ||
| if( preview == null ) { | ||
| - processor = createMarkdownProcessor( successor, context ); | ||
| + if( outputType == TEXT_TEX ) { | ||
| + processor = successor; | ||
| + } | ||
| + else { | ||
| + processor = createMarkdownProcessor( successor, context ); | ||
| + } | ||
| } | ||
| else { | ||
| final ProcessorContext context ) { | ||
| return createXhtmlProcessor( IDENTITY, context ); | ||
| + } | ||
| + | ||
| + private static Processor<String> createTextProcessor( | ||
| + final ProcessorContext context ) { | ||
| + return new TextProcessor( IDENTITY, context ); | ||
| } | ||
| -/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| - * | ||
| - * SPDX-License-Identifier: MIT | ||
| - */ | ||
| -package com.keenwrite.processors; | ||
| - | ||
| -import com.keenwrite.processors.r.RBootstrapController; | ||
| - | ||
| -public class RBootstrapProcessor extends ExecutorProcessor<String> { | ||
| - private final Processor<String> mSuccessor; | ||
| - private final ProcessorContext mContext; | ||
| - | ||
| - public RBootstrapProcessor( | ||
| - final Processor<String> successor, | ||
| - final ProcessorContext context ) { | ||
| - assert successor != null; | ||
| - assert context != null; | ||
| - | ||
| - mSuccessor = successor; | ||
| - mContext = context; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Processes the given text document by replacing variables with their values. | ||
| - * | ||
| - * @param text The document text that includes variables that should be | ||
| - * replaced with values when rendered as HTML. | ||
| - * @return The text with all variables replaced. | ||
| - */ | ||
| - @Override | ||
| - public String apply( final String text ) { | ||
| - assert text != null; | ||
| - | ||
| - final var bootstrap = mContext.getRScript(); | ||
| - final var workingDir = mContext.getRWorkingDir().toString(); | ||
| - final var definitions = mContext.getDefinitions(); | ||
| - | ||
| - RBootstrapController.update( bootstrap, workingDir, definitions ); | ||
| - | ||
| - return mSuccessor.apply( text ); | ||
| - } | ||
| -} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors; | ||
| - | ||
| -import com.keenwrite.sigils.SigilKeyOperator; | ||
| - | ||
| -import java.util.HashMap; | ||
| -import java.util.Map; | ||
| -import java.util.function.Function; | ||
| - | ||
| -import static com.keenwrite.processors.text.TextReplacementFactory.replace; | ||
| - | ||
| -/** | ||
| - * Processes interpolated string definitions in the document and inserts | ||
| - * their values into the post-processed text. The default variable syntax is | ||
| - * <pre>{{variable}}</pre> (a.k.a., moustache syntax). | ||
| - */ | ||
| -public class VariableProcessor | ||
| - extends ExecutorProcessor<String> implements Function<String, String> { | ||
| - | ||
| - private final ProcessorContext mContext; | ||
| - private final SigilKeyOperator mSigilOperator; | ||
| - | ||
| - /** | ||
| - * Constructs a processor capable of interpolating string definitions. | ||
| - * | ||
| - * @param successor Subsequent link in the processing chain. | ||
| - * @param context Contains resolved definitions map. | ||
| - */ | ||
| - public VariableProcessor( | ||
| - final Processor<String> successor, | ||
| - final ProcessorContext context ) { | ||
| - super( successor ); | ||
| - | ||
| - mContext = context; | ||
| - mSigilOperator = createKeyOperator( context ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Subclasses may change the type of operation performed on keys, such as | ||
| - * wrapping key names in sigils. | ||
| - * | ||
| - * @param context Provides the name of the file being edited. | ||
| - * @return An operator for transforming key names. | ||
| - */ | ||
| - protected SigilKeyOperator createKeyOperator( | ||
| - final ProcessorContext context ) { | ||
| - return context.createKeyOperator(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the map to use for variable substitution. | ||
| - * | ||
| - * @return A map of variable names to values, with keys wrapped in sigils. | ||
| - */ | ||
| - protected Map<String, String> getDefinitions() { | ||
| - return entoken( mContext.getInterpolatedDefinitions() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Subclasses may override this method to change how keys are wrapped | ||
| - * in sigils. | ||
| - * | ||
| - * @param key The key to enwrap. | ||
| - * @return The wrapped key. | ||
| - */ | ||
| - protected String processKey( final String key ) { | ||
| - return mSigilOperator.apply( key ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Subclasses may override this method to modify values prior to use. This | ||
| - * can be used, for example, to escape values prior to evaluating by a | ||
| - * scripting engine. | ||
| - * | ||
| - * @param value The value to process. | ||
| - * @return The processed value. | ||
| - */ | ||
| - protected String processValue( final String value ) { | ||
| - return value; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Answers whether the given key is wrapped in sigil tokens. | ||
| - * | ||
| - * @param key The key to analyze. | ||
| - * @return {@code true} if the key is wrapped in sigils. | ||
| - */ | ||
| - public boolean hasSigils( final String key ) { | ||
| - return mSigilOperator.match( key ).find(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Processes the given text document by replacing variables with their values. | ||
| - * | ||
| - * @param text The document text that includes variables that should be | ||
| - * replaced with values when rendered as HTML. | ||
| - * @return The text with all variables replaced. | ||
| - */ | ||
| - @Override | ||
| - public String apply( final String text ) { | ||
| - assert text != null; | ||
| - | ||
| - return replace( text, getDefinitions() ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Converts the given map from regular variables to processor-specific | ||
| - * variables. | ||
| - * | ||
| - * @param map Map of variable names to values. | ||
| - * @return Map of variables with the keys and values subjected to | ||
| - * post-processing. | ||
| - */ | ||
| - protected Map<String, String> entoken( final Map<String, String> map ) { | ||
| - assert map != null; | ||
| - | ||
| - final var result = new HashMap<String, String>( map.size() ); | ||
| - | ||
| - map.forEach( ( k, v ) -> result.put( processKey( k ), processValue( v ) ) ); | ||
| - | ||
| - return result; | ||
| - } | ||
| -} | ||
| -/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| - * | ||
| - * SPDX-License-Identifier: MIT | ||
| - */ | ||
| -package com.keenwrite.processors; | ||
| - | ||
| -import com.keenwrite.dom.DocumentParser; | ||
| -import com.keenwrite.io.MediaTypeExtension; | ||
| -import com.keenwrite.ui.heuristics.WordCounter; | ||
| -import com.keenwrite.util.DataTypeConverter; | ||
| -import com.whitemagicsoftware.keenquotes.parser.Contractions; | ||
| -import com.whitemagicsoftware.keenquotes.parser.Curler; | ||
| -import org.w3c.dom.Document; | ||
| - | ||
| -import java.io.File; | ||
| -import java.io.FileNotFoundException; | ||
| -import java.nio.file.Path; | ||
| -import java.util.*; | ||
| - | ||
| -import static com.keenwrite.Bootstrap.APP_TITLE_ABBR; | ||
| -import static com.keenwrite.dom.DocumentParser.*; | ||
| -import static com.keenwrite.events.StatusEvent.clue; | ||
| -import static com.keenwrite.io.SysFile.toFile; | ||
| -import static com.keenwrite.io.downloads.DownloadManager.open; | ||
| -import static com.keenwrite.util.ProtocolScheme.getProtocol; | ||
| -import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML; | ||
| -import static java.lang.String.format; | ||
| -import static java.lang.String.valueOf; | ||
| -import static java.nio.charset.StandardCharsets.UTF_8; | ||
| -import static java.nio.file.Files.copy; | ||
| -import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; | ||
| - | ||
| -/** | ||
| - * Responsible for making an XHTML document complete by wrapping it with html | ||
| - * and body elements. This doesn't have to be super-efficient because it's | ||
| - * not run in real-time. | ||
| - */ | ||
| -public final class XhtmlProcessor extends ExecutorProcessor<String> { | ||
| - private static final Curler sTypographer = | ||
| - new Curler( createContractions(), FILTER_XML, true ); | ||
| - | ||
| - private final ProcessorContext mContext; | ||
| - | ||
| - public XhtmlProcessor( | ||
| - final Processor<String> successor, final ProcessorContext context ) { | ||
| - super( successor ); | ||
| - | ||
| - assert context != null; | ||
| - mContext = context; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Responsible for producing a well-formed XML document complete with | ||
| - * metadata (title, author, keywords, copyright, and date). | ||
| - * | ||
| - * @param html The HTML document to transform into an XHTML document. | ||
| - * @return The transformed HTML document. | ||
| - */ | ||
| - @Override | ||
| - public String apply( final String html ) { | ||
| - clue( "Main.status.typeset.xhtml" ); | ||
| - | ||
| - try { | ||
| - final var doc = parse( html ); | ||
| - setMetaData( doc ); | ||
| - | ||
| - visit( doc, "//img", node -> { | ||
| - try { | ||
| - final var attrs = node.getAttributes(); | ||
| - final var attr = attrs.getNamedItem( "src" ); | ||
| - | ||
| - if( attr != null ) { | ||
| - final var src = attr.getTextContent(); | ||
| - final Path location; | ||
| - final Path imagesDir; | ||
| - | ||
| - // Download into a cache directory, which can be written to without | ||
| - // any possibility of overwriting local image files. Further, the | ||
| - // filenames are hashed as a second layer of protection. | ||
| - if( getProtocol( src ).isRemote() ) { | ||
| - location = downloadImage( src ); | ||
| - imagesDir = getCachesPath(); | ||
| - } | ||
| - else { | ||
| - location = resolveImage( src ); | ||
| - imagesDir = getImagesPath(); | ||
| - } | ||
| - | ||
| - final var relative = imagesDir.relativize( location ); | ||
| - | ||
| - attr.setTextContent( relative.toString() ); | ||
| - } | ||
| - } catch( final Exception ex ) { | ||
| - clue( ex ); | ||
| - } | ||
| - } ); | ||
| - | ||
| - final var document = DocumentParser.toString( doc ); | ||
| - final var curl = mContext.getCurlQuotes(); | ||
| - | ||
| - return curl ? sTypographer.apply( document ) : document; | ||
| - } catch( final Exception ex ) { | ||
| - clue( ex ); | ||
| - } | ||
| - | ||
| - return html; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Applies the metadata fields to the document. | ||
| - * | ||
| - * @param doc The document to adorn with metadata. | ||
| - */ | ||
| - private void setMetaData( final Document doc ) { | ||
| - final var metadata = createMetaDataMap( doc ); | ||
| - final var title = metadata.get( "title" ); | ||
| - | ||
| - visit( doc, "/html/head", node -> { | ||
| - // Insert <title>text</title> inside <head>. | ||
| - node.appendChild( createElement( doc, "title", title ) ); | ||
| - // Insert <meta charset="utf-8"> inside <head>. | ||
| - node.appendChild( createEncoding( doc, UTF_8.toString() ) ); | ||
| - | ||
| - // Insert each <meta name=x content=y /> inside <head>. | ||
| - metadata.entrySet().forEach( | ||
| - entry -> node.appendChild( createMeta( doc, entry ) ) | ||
| - ); | ||
| - } ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Generates document metadata, including word count. | ||
| - * | ||
| - * @param doc The document containing the text to tally. | ||
| - * @return A map of metadata key/value pairs. | ||
| - */ | ||
| - private Map<String, String> createMetaDataMap( final Document doc ) { | ||
| - final var result = new LinkedHashMap<String, String>(); | ||
| - final var map = mContext.getInterpolatedDefinitions(); | ||
| - final var metadata = getMetadata(); | ||
| - | ||
| - metadata.forEach( | ||
| - ( key, value ) -> { | ||
| - final var interpolated = map.interpolate( value ); | ||
| - | ||
| - if( !interpolated.isEmpty() ) { | ||
| - result.put( key, interpolated ); | ||
| - } | ||
| - } | ||
| - ); | ||
| - result.put( "count", wordCount( doc ) ); | ||
| - | ||
| - return result; | ||
| - } | ||
| - | ||
| - /** | ||
| - * The metadata is in list form because the user interface for entering the | ||
| - * key-value pairs is a table, which requires a generic {@link List} rather | ||
| - * than a generic {@link Map}. | ||
| - * | ||
| - * @return The document metadata. | ||
| - */ | ||
| - private Map<String, String> getMetadata() { | ||
| - final var result = mContext.getMetadata(); | ||
| - return result == null ? new HashMap<>() : result; | ||
| - } | ||
| - | ||
| - /** | ||
| - * Hashes the URL so that the number of files doesn't eat up disk space | ||
| - * over time. For static resources, a feature could be added to prevent | ||
| - * downloading the URL if the hashed filename already exists. | ||
| - * | ||
| - * @param src The source file's URL to download. | ||
| - * @return A {@link Path} to the local file containing the URL's contents. | ||
| - * @throws Exception Could not download or save the file. | ||
| - */ | ||
| - private Path downloadImage( final String src ) throws Exception { | ||
| - final Path imagePath; | ||
| - final File imageFile; | ||
| - final var cachesPath = getCachesPath(); | ||
| - | ||
| - clue( "Main.status.image.xhtml.image.download", src ); | ||
| - | ||
| - try( final var response = open( src ) ) { | ||
| - final var mediaType = response.getMediaType(); | ||
| - | ||
| - final var ext = MediaTypeExtension.valueFrom( mediaType ).getExtension(); | ||
| - final var hash = DataTypeConverter.toHex( DataTypeConverter.hash( src ) ); | ||
| - final var id = hash.toLowerCase(); | ||
| - | ||
| - imagePath = cachesPath.resolve( APP_TITLE_ABBR + id + '.' + ext ); | ||
| - imageFile = toFile( imagePath ); | ||
| - | ||
| - // Preserve image files if auto-remove is turned off. | ||
| - if( autoRemove() ) { | ||
| - imageFile.deleteOnExit(); | ||
| - } | ||
| - | ||
| - try( final var image = response.getInputStream() ) { | ||
| - copy( image, imagePath, REPLACE_EXISTING ); | ||
| - } | ||
| - | ||
| - if( mediaType.isSvg() ) { | ||
| - sanitize( imagePath ); | ||
| - } | ||
| - } | ||
| - | ||
| - final var key = imageFile.exists() | ||
| - ? "Main.status.image.xhtml.image.saved" | ||
| - : "Main.status.image.xhtml.image.failed"; | ||
| - clue( key, imageFile ); | ||
| - | ||
| - return imagePath; | ||
| - } | ||
| - | ||
| - private Path resolveImage( final String src ) throws Exception { | ||
| - var imagePath = getImagesPath(); | ||
| - var found = false; | ||
| - | ||
| - Path imageFile = null; | ||
| - | ||
| - clue( "Main.status.image.xhtml.image.resolve", src ); | ||
| - | ||
| - for( final var extension : getImageOrder() ) { | ||
| - final var filename = format( | ||
| - "%s%s%s", src, extension.isBlank() ? "" : ".", extension ); | ||
| - imageFile = imagePath.resolve( filename ); | ||
| - | ||
| - if( toFile( imageFile ).exists() ) { | ||
| - found = true; | ||
| - break; | ||
| - } | ||
| - } | ||
| - | ||
| - if( !found ) { | ||
| - imagePath = getDocumentDir(); | ||
| - imageFile = imagePath.resolve( src ); | ||
| - | ||
| - if( !toFile( imageFile ).exists() ) { | ||
| - final var filename = imageFile.toString(); | ||
| - clue( "Main.status.image.xhtml.image.missing", filename ); | ||
| - | ||
| - throw new FileNotFoundException( filename ); | ||
| - } | ||
| - } | ||
| - | ||
| - clue( "Main.status.image.xhtml.image.found", imageFile.toString() ); | ||
| - | ||
| - return imageFile; | ||
| - } | ||
| - | ||
| - private Path getImagesPath() { | ||
| - return mContext.getImageDir(); | ||
| - } | ||
| - | ||
| - private Path getCachesPath() { | ||
| - return mContext.getCacheDir(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * By including an "empty" extension, the first element returned | ||
| - * will be the empty string. Thus, the first extension to try is the | ||
| - * file's default extension. Subsequent iterations will try to find | ||
| - * a file that has a name matching one of the preferred extensions. | ||
| - * | ||
| - * @return A list of extensions, including an empty string at the start. | ||
| - */ | ||
| - private Iterable<String> getImageOrder() { | ||
| - return mContext.getImageOrder(); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Returns the absolute path to the document being edited, which can be used | ||
| - * to find files included using relative paths. | ||
| - * | ||
| - * @return The directory containing the edited file. | ||
| - */ | ||
| - private Path getDocumentDir() { | ||
| - return mContext.getBaseDir(); | ||
| - } | ||
| - | ||
| - private Locale getLocale() { | ||
| - return mContext.getLocale(); | ||
| - } | ||
| - | ||
| - private boolean autoRemove() { | ||
| - return mContext.getAutoRemove(); | ||
| - } | ||
| - | ||
| - private String wordCount( final Document doc ) { | ||
| - final var sb = new StringBuilder( 65536 * 10 ); | ||
| - | ||
| - visit( | ||
| - doc, | ||
| - "//*[normalize-space( text() ) != '']", | ||
| - node -> sb.append( node.getTextContent() ) | ||
| - ); | ||
| - | ||
| - return valueOf( WordCounter.create( getLocale() ).count( sb.toString() ) ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * Creates contracts with a custom set of unambiguous strings. | ||
| - * | ||
| - * @return List of contractions to use for curling straight quotes. | ||
| - */ | ||
| - private static Contractions createContractions() { | ||
| - return new Contractions.Builder().build(); | ||
| - } | ||
| -} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.processors.html; | ||
| + | ||
| +import com.keenwrite.preview.HtmlPreview; | ||
| +import com.keenwrite.processors.ExecutorProcessor; | ||
| + | ||
| +/** | ||
| + * Responsible for notifying the {@link HtmlPreview} when the succession | ||
| + * chain has updated. This decouples knowledge of changes to the editor panel | ||
| + * from the HTML preview panel as well as any processing that takes place | ||
| + * before the final HTML preview is rendered. This is the last link in the | ||
| + * processor chain. | ||
| + */ | ||
| +public final class HtmlPreviewProcessor extends ExecutorProcessor<String> { | ||
| + /** | ||
| + * There is only one preview panel. | ||
| + */ | ||
| + private static HtmlPreview sHtmlPreview; | ||
| + | ||
| + /** | ||
| + * Constructs the end of a processing chain. | ||
| + * | ||
| + * @param htmlPreview The pane to update with the post-processed document. | ||
| + */ | ||
| + public HtmlPreviewProcessor( final HtmlPreview htmlPreview ) { | ||
| + sHtmlPreview = htmlPreview; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Update the preview panel using HTML from the succession chain. | ||
| + * | ||
| + * @param html The document content to render in the preview pane. The HTML | ||
| + * should not contain a doctype, head, or body tag. | ||
| + * @return The given {@code html} string. | ||
| + */ | ||
| + @Override | ||
| + public String apply( final String html ) { | ||
| + assert html != null; | ||
| + | ||
| + sHtmlPreview.render( html ); | ||
| + return html; | ||
| + } | ||
| +} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.processors.html; | ||
| + | ||
| +import com.keenwrite.processors.ExecutorProcessor; | ||
| + | ||
| +/** | ||
| + * Responsible for transforming a string into itself. This is used at the | ||
| + * end of a processing chain when no more processing is required. | ||
| + */ | ||
| +public final class IdentityProcessor extends ExecutorProcessor<String> { | ||
| + public static final IdentityProcessor IDENTITY = new IdentityProcessor(); | ||
| + | ||
| + /** | ||
| + * Constructs a new instance having no successor (the default successor is | ||
| + * {@code null}). | ||
| + */ | ||
| + private IdentityProcessor() { | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the given string without modification. | ||
| + * | ||
| + * @param s The string to return. | ||
| + * @return The value of s. | ||
| + */ | ||
| + @Override | ||
| + public String apply( final String s ) { | ||
| + return s; | ||
| + } | ||
| +} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.html; | ||
| + | ||
| +import com.keenwrite.processors.ExecutorProcessor; | ||
| +import com.keenwrite.processors.Processor; | ||
| + | ||
| +/** | ||
| + * This is the default processor used when an unknown file name extension is | ||
| + * encountered. It processes the text by enclosing it in an HTML {@code <pre>} | ||
| + * element. | ||
| + */ | ||
| +public final class PreformattedProcessor extends ExecutorProcessor<String> { | ||
| + | ||
| + /** | ||
| + * Passes the link to the super constructor. | ||
| + * | ||
| + * @param successor The next processor in the chain to use for text | ||
| + * processing. | ||
| + */ | ||
| + public PreformattedProcessor( final Processor<String> successor ) { | ||
| + super( successor ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the given string, modified with "pre" tags. | ||
| + * | ||
| + * @param t The string to return, enclosed in "pre" tags. | ||
| + * @return The value of t wrapped in "pre" tags. | ||
| + */ | ||
| + @Override | ||
| + public String apply( final String t ) { | ||
| + return STR."<pre>\{t}</pre>"; | ||
| + } | ||
| +} | ||
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.html; | ||
| + | ||
| +import com.keenwrite.dom.DocumentParser; | ||
| +import com.keenwrite.io.MediaTypeExtension; | ||
| +import com.keenwrite.processors.ExecutorProcessor; | ||
| +import com.keenwrite.processors.Processor; | ||
| +import com.keenwrite.processors.ProcessorContext; | ||
| +import com.keenwrite.ui.heuristics.WordCounter; | ||
| +import com.keenwrite.util.DataTypeConverter; | ||
| +import com.whitemagicsoftware.keenquotes.parser.Contractions; | ||
| +import com.whitemagicsoftware.keenquotes.parser.Curler; | ||
| +import org.w3c.dom.Document; | ||
| + | ||
| +import java.io.File; | ||
| +import java.io.FileNotFoundException; | ||
| +import java.nio.file.Path; | ||
| +import java.util.*; | ||
| + | ||
| +import static com.keenwrite.Bootstrap.APP_TITLE_ABBR; | ||
| +import static com.keenwrite.dom.DocumentParser.*; | ||
| +import static com.keenwrite.events.StatusEvent.clue; | ||
| +import static com.keenwrite.io.SysFile.toFile; | ||
| +import static com.keenwrite.io.downloads.DownloadManager.open; | ||
| +import static com.keenwrite.util.ProtocolScheme.getProtocol; | ||
| +import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML; | ||
| +import static java.lang.String.format; | ||
| +import static java.lang.String.valueOf; | ||
| +import static java.nio.charset.StandardCharsets.UTF_8; | ||
| +import static java.nio.file.Files.copy; | ||
| +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; | ||
| + | ||
| +/** | ||
| + * Responsible for making an XHTML document complete by wrapping it with html | ||
| + * and body elements. This doesn't have to be super-efficient because it's | ||
| + * not run in real time. | ||
| + */ | ||
| +public final class XhtmlProcessor extends ExecutorProcessor<String> { | ||
| + private static final Curler sTypographer = | ||
| + new Curler( createContractions(), FILTER_XML, true ); | ||
| + | ||
| + private final ProcessorContext mContext; | ||
| + | ||
| + public XhtmlProcessor( | ||
| + final Processor<String> successor, final ProcessorContext context ) { | ||
| + super( successor ); | ||
| + | ||
| + assert context != null; | ||
| + mContext = context; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Responsible for producing a well-formed XML document complete with | ||
| + * metadata (title, author, keywords, copyright, and date). | ||
| + * | ||
| + * @param html The HTML document to transform into an XHTML document. | ||
| + * @return The transformed HTML document. | ||
| + */ | ||
| + @Override | ||
| + public String apply( final String html ) { | ||
| + clue( "Main.status.typeset.xhtml" ); | ||
| + | ||
| + try { | ||
| + final var doc = parse( html ); | ||
| + setMetaData( doc ); | ||
| + | ||
| + visit( doc, "//img", node -> { | ||
| + try { | ||
| + final var attrs = node.getAttributes(); | ||
| + final var attr = attrs.getNamedItem( "src" ); | ||
| + | ||
| + if( attr != null ) { | ||
| + final var src = attr.getTextContent(); | ||
| + final Path location; | ||
| + final Path imagesDir; | ||
| + | ||
| + // Download into a cache directory, which can be written to without | ||
| + // any possibility of overwriting local image files. Further, the | ||
| + // filenames are hashed as a second layer of protection. | ||
| + if( getProtocol( src ).isRemote() ) { | ||
| + location = downloadImage( src ); | ||
| + imagesDir = getCachesPath(); | ||
| + } | ||
| + else { | ||
| + location = resolveImage( src ); | ||
| + imagesDir = getImagesPath(); | ||
| + } | ||
| + | ||
| + final var relative = imagesDir.relativize( location ); | ||
| + | ||
| + attr.setTextContent( relative.toString() ); | ||
| + } | ||
| + } catch( final Exception ex ) { | ||
| + clue( ex ); | ||
| + } | ||
| + } ); | ||
| + | ||
| + final var document = DocumentParser.toString( doc ); | ||
| + final var curl = mContext.getCurlQuotes(); | ||
| + | ||
| + return curl ? sTypographer.apply( document ) : document; | ||
| + } catch( final Exception ex ) { | ||
| + clue( ex ); | ||
| + } | ||
| + | ||
| + return html; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Applies the metadata fields to the document. | ||
| + * | ||
| + * @param doc The document to adorn with metadata. | ||
| + */ | ||
| + private void setMetaData( final Document doc ) { | ||
| + final var metadata = createMetaDataMap( doc ); | ||
| + final var title = metadata.get( "title" ); | ||
| + | ||
| + visit( doc, "/html/head", node -> { | ||
| + // Insert <title>text</title> inside <head>. | ||
| + node.appendChild( createElement( doc, "title", title ) ); | ||
| + // Insert <meta charset="utf-8"> inside <head>. | ||
| + node.appendChild( createEncoding( doc, UTF_8.toString() ) ); | ||
| + | ||
| + // Insert each <meta name=x content=y /> inside <head>. | ||
| + metadata.entrySet().forEach( | ||
| + entry -> node.appendChild( createMeta( doc, entry ) ) | ||
| + ); | ||
| + } ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Generates document metadata, including word count. | ||
| + * | ||
| + * @param doc The document containing the text to tally. | ||
| + * @return A map of metadata key/value pairs. | ||
| + */ | ||
| + private Map<String, String> createMetaDataMap( final Document doc ) { | ||
| + final var result = new LinkedHashMap<String, String>(); | ||
| + final var map = mContext.getInterpolatedDefinitions(); | ||
| + final var metadata = getMetadata(); | ||
| + | ||
| + metadata.forEach( | ||
| + ( key, value ) -> { | ||
| + final var interpolated = map.interpolate( value ); | ||
| + | ||
| + if( !interpolated.isEmpty() ) { | ||
| + result.put( key, interpolated ); | ||
| + } | ||
| + } | ||
| + ); | ||
| + result.put( "count", wordCount( doc ) ); | ||
| + | ||
| + return result; | ||
| + } | ||
| + | ||
| + /** | ||
| + * The metadata is in list form because the user interface for entering the | ||
| + * key-value pairs is a table, which requires a generic {@link List} rather | ||
| + * than a generic {@link Map}. | ||
| + * | ||
| + * @return The document metadata. | ||
| + */ | ||
| + private Map<String, String> getMetadata() { | ||
| + final var result = mContext.getMetadata(); | ||
| + return result == null ? new HashMap<>() : result; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Hashes the URL so that the number of files doesn't eat up disk space | ||
| + * over time. For static resources, a feature could be added to prevent | ||
| + * downloading the URL if the hashed filename already exists. | ||
| + * | ||
| + * @param src The source file's URL to download. | ||
| + * @return A {@link Path} to the local file containing the URL's contents. | ||
| + * @throws Exception Could not download or save the file. | ||
| + */ | ||
| + private Path downloadImage( final String src ) throws Exception { | ||
| + final Path imagePath; | ||
| + final File imageFile; | ||
| + final var cachesPath = getCachesPath(); | ||
| + | ||
| + clue( "Main.status.image.xhtml.image.download", src ); | ||
| + | ||
| + try( final var response = open( src ) ) { | ||
| + final var mediaType = response.getMediaType(); | ||
| + | ||
| + final var ext = MediaTypeExtension.valueFrom( mediaType ).getExtension(); | ||
| + final var hash = DataTypeConverter.toHex( DataTypeConverter.hash( src ) ); | ||
| + final var id = hash.toLowerCase(); | ||
| + | ||
| + imagePath = cachesPath.resolve( APP_TITLE_ABBR + id + '.' + ext ); | ||
| + imageFile = toFile( imagePath ); | ||
| + | ||
| + // Preserve image files if auto-remove is turned off. | ||
| + if( autoRemove() ) { | ||
| + imageFile.deleteOnExit(); | ||
| + } | ||
| + | ||
| + try( final var image = response.getInputStream() ) { | ||
| + copy( image, imagePath, REPLACE_EXISTING ); | ||
| + } | ||
| + | ||
| + if( mediaType.isSvg() ) { | ||
| + sanitize( imagePath ); | ||
| + } | ||
| + } | ||
| + | ||
| + final var key = imageFile.exists() | ||
| + ? "Main.status.image.xhtml.image.saved" | ||
| + : "Main.status.image.xhtml.image.failed"; | ||
| + clue( key, imageFile ); | ||
| + | ||
| + return imagePath; | ||
| + } | ||
| + | ||
| + private Path resolveImage( final String src ) throws Exception { | ||
| + var imagePath = getImagesPath(); | ||
| + var found = false; | ||
| + | ||
| + Path imageFile = null; | ||
| + | ||
| + clue( "Main.status.image.xhtml.image.resolve", src ); | ||
| + | ||
| + for( final var extension : getImageOrder() ) { | ||
| + final var filename = format( | ||
| + "%s%s%s", src, extension.isBlank() ? "" : ".", extension ); | ||
| + imageFile = imagePath.resolve( filename ); | ||
| + | ||
| + if( toFile( imageFile ).exists() ) { | ||
| + found = true; | ||
| + break; | ||
| + } | ||
| + } | ||
| + | ||
| + if( !found ) { | ||
| + imagePath = getDocumentDir(); | ||
| + imageFile = imagePath.resolve( src ); | ||
| + | ||
| + if( !toFile( imageFile ).exists() ) { | ||
| + final var filename = imageFile.toString(); | ||
| + clue( "Main.status.image.xhtml.image.missing", filename ); | ||
| + | ||
| + throw new FileNotFoundException( filename ); | ||
| + } | ||
| + } | ||
| + | ||
| + clue( "Main.status.image.xhtml.image.found", imageFile.toString() ); | ||
| + | ||
| + return imageFile; | ||
| + } | ||
| + | ||
| + private Path getImagesPath() { | ||
| + return mContext.getImageDir(); | ||
| + } | ||
| + | ||
| + private Path getCachesPath() { | ||
| + return mContext.getCacheDir(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * By including an "empty" extension, the first element returned | ||
| + * will be the empty string. Thus, the first extension to try is the | ||
| + * file's default extension. Subsequent iterations will try to find | ||
| + * a file that has a name matching one of the preferred extensions. | ||
| + * | ||
| + * @return A list of extensions, including an empty string at the start. | ||
| + */ | ||
| + private Iterable<String> getImageOrder() { | ||
| + return mContext.getImageOrder(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the absolute path to the document being edited, which can be used | ||
| + * to find files included using relative paths. | ||
| + * | ||
| + * @return The directory containing the edited file. | ||
| + */ | ||
| + private Path getDocumentDir() { | ||
| + return mContext.getBaseDir(); | ||
| + } | ||
| + | ||
| + private Locale getLocale() { | ||
| + return mContext.getLocale(); | ||
| + } | ||
| + | ||
| + private boolean autoRemove() { | ||
| + return mContext.getAutoRemove(); | ||
| + } | ||
| + | ||
| + private String wordCount( final Document doc ) { | ||
| + final var sb = new StringBuilder( 65536 * 10 ); | ||
| + | ||
| + visit( | ||
| + doc, | ||
| + "//*[normalize-space( text() ) != '']", | ||
| + node -> sb.append( node.getTextContent() ) | ||
| + ); | ||
| + | ||
| + return valueOf( WordCounter.create( getLocale() ).count( sb.toString() ) ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Creates contracts with a custom set of unambiguous strings. | ||
| + * | ||
| + * @return List of contractions to use for curling straight quotes. | ||
| + */ | ||
| + private static Contractions createContractions() { | ||
| + return new Contractions.Builder().build(); | ||
| + } | ||
| +} | ||
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| package com.keenwrite.processors.markdown; | ||
| import com.keenwrite.processors.Processor; | ||
| import com.keenwrite.processors.ProcessorContext; | ||
| -import com.keenwrite.processors.VariableProcessor; | ||
| +import com.keenwrite.processors.variable.VariableProcessor; | ||
| import com.keenwrite.processors.markdown.extensions.caret.CaretExtension; | ||
| import com.keenwrite.processors.markdown.extensions.fences.FencedBlockExtension; | ||
| import com.keenwrite.processors.markdown.extensions.images.ImageLinkExtension; | ||
| import com.keenwrite.processors.markdown.extensions.outline.DocumentOutlineExtension; | ||
| import com.keenwrite.processors.markdown.extensions.r.RInlineExtension; | ||
| import com.keenwrite.processors.markdown.extensions.tex.TexExtension; | ||
| import com.keenwrite.processors.r.RInlineEvaluator; | ||
| -import com.keenwrite.processors.r.RVariableProcessor; | ||
| +import com.keenwrite.processors.variable.RVariableProcessor; | ||
| import com.vladsch.flexmark.util.misc.Extension; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.function.Function; | ||
| import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN; | ||
| -import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | ||
| +import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | ||
| /** |
| import com.keenwrite.processors.Processor; | ||
| import com.keenwrite.processors.ProcessorContext; | ||
| -import com.keenwrite.processors.VariableProcessor; | ||
| +import com.keenwrite.processors.variable.VariableProcessor; | ||
| import com.keenwrite.processors.markdown.MarkdownProcessor; | ||
| import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter; | ||
| import com.keenwrite.processors.r.RChunkEvaluator; | ||
| -import com.keenwrite.processors.r.RVariableProcessor; | ||
| +import com.keenwrite.processors.variable.RVariableProcessor; | ||
| import com.vladsch.flexmark.ast.FencedCodeBlock; | ||
| import com.vladsch.flexmark.html.HtmlRendererOptions; | ||
| import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; | ||
| import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY; | ||
| -import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | ||
| +import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | ||
| import static com.vladsch.flexmark.html.HtmlRenderer.Builder; | ||
| import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT; | ||
| import java.util.Map; | ||
| -import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | ||
| +import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | ||
| import static com.vladsch.flexmark.parser.Parser.Builder; | ||
| HTML_TEX_DELIMITED, new TexDelimitedNodeRenderer(), | ||
| XHTML_TEX, new TexElementNodeRenderer( true ), | ||
| + TEXT_TEX, new TexElementNodeRenderer( true ), | ||
| NONE, RENDERER | ||
| ); |
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.processors.pdf; | ||
| + | ||
| +import com.keenwrite.processors.ExecutorProcessor; | ||
| +import com.keenwrite.processors.ProcessorContext; | ||
| +import com.keenwrite.typesetting.Typesetter; | ||
| + | ||
| +import static com.keenwrite.Bootstrap.APP_TITLE_ABBR; | ||
| +import static com.keenwrite.events.StatusEvent.clue; | ||
| +import static com.keenwrite.io.MediaType.TEXT_XML; | ||
| +import static com.keenwrite.io.SysFile.normalize; | ||
| +import static com.keenwrite.typesetting.Typesetter.Mutator; | ||
| +import static com.keenwrite.util.Strings.sanitize; | ||
| +import static java.nio.charset.StandardCharsets.UTF_8; | ||
| +import static java.nio.file.Files.deleteIfExists; | ||
| +import static java.nio.file.Files.writeString; | ||
| + | ||
| +/** | ||
| + * Responsible for using a typesetting engine to convert an XHTML document | ||
| + * into a PDF file. This must not be run from the JavaFX thread. | ||
| + */ | ||
| +public final class PdfProcessor extends ExecutorProcessor<String> { | ||
| + private final ProcessorContext mProcessorContext; | ||
| + | ||
| + public PdfProcessor( final ProcessorContext context ) { | ||
| + assert context != null; | ||
| + mProcessorContext = context; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Converts a document by calling a third-party application to typeset the | ||
| + * given XHTML document. | ||
| + * | ||
| + * @param xhtml The document to convert to a PDF file. | ||
| + * @return {@code null} because there is no valid return value from generating | ||
| + * a PDF file. | ||
| + */ | ||
| + public String apply( final String xhtml ) { | ||
| + try { | ||
| + clue( "Main.status.typeset.create" ); | ||
| + | ||
| + final var context = mProcessorContext; | ||
| + final var targetPath = context.getTargetPath(); | ||
| + clue( "Main.status.typeset.setting", "target", targetPath ); | ||
| + | ||
| + final var parent = normalize( targetPath.toAbsolutePath().getParent() ); | ||
| + | ||
| + final var document = TEXT_XML.createTempFile( APP_TITLE_ABBR, parent ); | ||
| + final var sourcePath = writeString( document, xhtml, UTF_8 ); | ||
| + clue( "Main.status.typeset.setting", "source", sourcePath ); | ||
| + | ||
| + final var themeDir = normalize( context.getThemeDir() ); | ||
| + clue( "Main.status.typeset.setting", "themes", themeDir ); | ||
| + | ||
| + final var imageDir = normalize( context.getImageDir() ); | ||
| + clue( "Main.status.typeset.setting", "images", imageDir ); | ||
| + | ||
| + final var imageOrder = context.getImageOrder(); | ||
| + clue( "Main.status.typeset.setting", "order", imageOrder ); | ||
| + | ||
| + final var cacheDir = normalize( context.getCacheDir() ); | ||
| + clue( "Main.status.typeset.setting", "caches", cacheDir ); | ||
| + | ||
| + final var fontDir = normalize( context.getFontDir() ); | ||
| + clue( "Main.status.typeset.setting", "fonts", fontDir ); | ||
| + | ||
| + final var rWorkDir = normalize( context.getRWorkingDir() ); | ||
| + clue( "Main.status.typeset.setting", "r-work", rWorkDir ); | ||
| + | ||
| + final var modesEnabled = sanitize( context.getModesEnabled() ); | ||
| + clue( "Main.status.typeset.setting", "mode", modesEnabled ); | ||
| + | ||
| + final var autoRemove = context.getAutoRemove(); | ||
| + clue( "Main.status.typeset.setting", "purge", autoRemove ); | ||
| + | ||
| + final var typesetter = Typesetter | ||
| + .builder() | ||
| + .with( Mutator::setTargetPath, targetPath ) | ||
| + .with( Mutator::setSourcePath, sourcePath ) | ||
| + .with( Mutator::setThemeDir, themeDir ) | ||
| + .with( Mutator::setImageDir, imageDir ) | ||
| + .with( Mutator::setCacheDir, cacheDir ) | ||
| + .with( Mutator::setFontDir, fontDir ) | ||
| + .with( Mutator::setModesEnabled, modesEnabled ) | ||
| + .with( Mutator::setAutoRemove, autoRemove ) | ||
| + .build(); | ||
| + | ||
| + try { | ||
| + typesetter.typeset(); | ||
| + } | ||
| + finally { | ||
| + // Smote the temporary file after typesetting the document. | ||
| + if( typesetter.autoRemove() ) { | ||
| + deleteIfExists( document ); | ||
| + } | ||
| + } | ||
| + } catch( final Exception ex ) { | ||
| + // Typesetter runtime exceptions will pass up the call stack. | ||
| + clue( "Main.status.typeset.failed", ex ); | ||
| + } | ||
| + | ||
| + // Do not continue processing (the document was typeset into a binary). | ||
| + return null; | ||
| + } | ||
| +} | ||
| import static com.keenwrite.preferences.AppKeys.KEY_R_DIR; | ||
| import static com.keenwrite.preferences.AppKeys.KEY_R_SCRIPT; | ||
| -import static com.keenwrite.processors.r.RVariableProcessor.escape; | ||
| +import static com.keenwrite.processors.variable.RVariableProcessor.escape; | ||
| import static com.keenwrite.processors.text.TextReplacementFactory.replace; | ||
| +/* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.r; | ||
| + | ||
| +import com.keenwrite.processors.ExecutorProcessor; | ||
| +import com.keenwrite.processors.Processor; | ||
| +import com.keenwrite.processors.ProcessorContext; | ||
| + | ||
| +public class RBootstrapProcessor extends ExecutorProcessor<String> { | ||
| + private final Processor<String> mSuccessor; | ||
| + private final ProcessorContext mContext; | ||
| + | ||
| + public RBootstrapProcessor( | ||
| + final Processor<String> successor, | ||
| + final ProcessorContext context ) { | ||
| + assert successor != null; | ||
| + assert context != null; | ||
| + | ||
| + mSuccessor = successor; | ||
| + mContext = context; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Processes the given text document by replacing variables with their values. | ||
| + * | ||
| + * @param text The document text that includes variables that should be | ||
| + * replaced with values when rendered as HTML. | ||
| + * @return The text with all variables replaced. | ||
| + */ | ||
| + @Override | ||
| + public String apply( final String text ) { | ||
| + assert text != null; | ||
| + | ||
| + final var bootstrap = mContext.getRScript(); | ||
| + final var workingDir = mContext.getRWorkingDir().toString(); | ||
| + final var definitions = mContext.getDefinitions(); | ||
| + | ||
| + RBootstrapController.update( bootstrap, workingDir, definitions ); | ||
| + | ||
| + return mSuccessor.apply( text ); | ||
| + } | ||
| +} | ||
| import com.keenwrite.processors.Processor; | ||
| +import com.keenwrite.processors.variable.RVariableProcessor; | ||
| import java.util.function.Function; |
| -/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| -package com.keenwrite.processors.r; | ||
| - | ||
| -import com.keenwrite.processors.Processor; | ||
| -import com.keenwrite.processors.ProcessorContext; | ||
| -import com.keenwrite.processors.VariableProcessor; | ||
| -import com.keenwrite.sigils.RKeyOperator; | ||
| -import com.keenwrite.sigils.SigilKeyOperator; | ||
| - | ||
| -import java.util.function.UnaryOperator; | ||
| - | ||
| -/** | ||
| - * Converts the keys of the resolved map from default form to R form, then | ||
| - * performs a substitution on the text. The default R variable syntax is | ||
| - * <pre>v$tree$leaf</pre>. | ||
| - */ | ||
| -public class RVariableProcessor extends VariableProcessor { | ||
| - public RVariableProcessor( | ||
| - final Processor<String> successor, final ProcessorContext context ) { | ||
| - super( successor, context ); | ||
| - } | ||
| - | ||
| - @Override | ||
| - protected SigilKeyOperator createKeyOperator( | ||
| - final ProcessorContext context ) { | ||
| - return new RKeyOperator(); | ||
| - } | ||
| - | ||
| - @Override | ||
| - protected String processValue( final String value ) { | ||
| - assert value != null; | ||
| - | ||
| - return escape( value ); | ||
| - } | ||
| - | ||
| - /** | ||
| - * In R, single quotes and double quotes are interchangeable. Using single | ||
| - * quotes is simpler to code. | ||
| - * | ||
| - * @param value The text to convert into a valid quoted R string. | ||
| - * @return The quoted value with embedded quotes escaped as necessary. | ||
| - */ | ||
| - public static String escape( final String value ) { | ||
| - return '\'' + escape( value, '\'', "\\'" ) + '\''; | ||
| - } | ||
| - | ||
| - /** | ||
| - * TODO: Make generic method for replacing text. | ||
| - * | ||
| - * @param haystack Search this string for the needle, must not be null. | ||
| - * @param needle The character to find in the haystack. | ||
| - * @param thread Replace the needle with this text, if the needle is found. | ||
| - * @return The haystack with the all instances of needle replaced with thread. | ||
| - */ | ||
| - @SuppressWarnings( "SameParameterValue" ) | ||
| - private static String escape( | ||
| - final String haystack, final char needle, final String thread ) { | ||
| - assert haystack != null; | ||
| - assert thread != null; | ||
| - | ||
| - int end = haystack.indexOf( needle ); | ||
| - | ||
| - if( end < 0 ) { | ||
| - return haystack; | ||
| - } | ||
| - | ||
| - int start = 0; | ||
| - | ||
| - // Replace up to 32 occurrences before reallocating the internal buffer. | ||
| - final var sb = new StringBuilder( haystack.length() + 32 ); | ||
| - | ||
| - while( end >= 0 ) { | ||
| - sb.append( haystack, start, end ).append( thread ); | ||
| - start = end + 1; | ||
| - end = haystack.indexOf( needle, start ); | ||
| - } | ||
| - | ||
| - return sb.append( haystack.substring( start ) ).toString(); | ||
| - } | ||
| -} | ||
| +/* Copyright 2024 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.text; | ||
| + | ||
| +import com.keenwrite.io.MediaType; | ||
| +import com.keenwrite.processors.ExecutorProcessor; | ||
| +import com.keenwrite.processors.Processor; | ||
| +import com.keenwrite.processors.ProcessorContext; | ||
| +import com.keenwrite.processors.r.RInlineEvaluator; | ||
| +import com.keenwrite.processors.variable.RVariableProcessor; | ||
| +import com.keenwrite.processors.variable.VariableProcessor; | ||
| + | ||
| +import java.util.function.Function; | ||
| + | ||
| +import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN; | ||
| +import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | ||
| + | ||
| +/** | ||
| + * Responsible for converting documents to plain text files. This will | ||
| + * perform interpolated variable substitutions and execute R commands | ||
| + * as necessary. | ||
| + */ | ||
| +public class TextProcessor extends ExecutorProcessor<String> { | ||
| + private final Function<String, String> mEvaluator; | ||
| + | ||
| + public TextProcessor( | ||
| + final Processor<String> successor, | ||
| + final ProcessorContext context ) { | ||
| + super( successor ); | ||
| + | ||
| + final var inputPath = context.getSourcePath(); | ||
| + final var mediaType = MediaType.fromFilename( inputPath ); | ||
| + | ||
| + if( mediaType == TEXT_R_MARKDOWN ) { | ||
| + final var rVarProcessor = new RVariableProcessor( IDENTITY, context ); | ||
| + mEvaluator = new RInlineEvaluator( rVarProcessor ); | ||
| + } | ||
| + else { | ||
| + mEvaluator = new VariableProcessor( IDENTITY, context ); | ||
| + } | ||
| + } | ||
| + | ||
| + @Override | ||
| + public String apply( final String document ) { | ||
| + return mEvaluator.apply( document ); | ||
| + } | ||
| +} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. | ||
| + * | ||
| + * SPDX-License-Identifier: MIT | ||
| + */ | ||
| +package com.keenwrite.processors.variable; | ||
| + | ||
| +import com.keenwrite.processors.Processor; | ||
| +import com.keenwrite.processors.ProcessorContext; | ||
| +import com.keenwrite.sigils.RKeyOperator; | ||
| +import com.keenwrite.sigils.SigilKeyOperator; | ||
| + | ||
| +/** | ||
| + * Converts the keys of the resolved map from default form to R form, then | ||
| + * performs a substitution on the text. The default R variable syntax is | ||
| + * <pre>v$tree$leaf</pre>. | ||
| + */ | ||
| +public class RVariableProcessor extends VariableProcessor { | ||
| + public RVariableProcessor( | ||
| + final Processor<String> successor, final ProcessorContext context ) { | ||
| + super( successor, context ); | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected SigilKeyOperator createKeyOperator( | ||
| + final ProcessorContext context ) { | ||
| + return new RKeyOperator(); | ||
| + } | ||
| + | ||
| + @Override | ||
| + protected String processValue( final String value ) { | ||
| + assert value != null; | ||
| + | ||
| + return escape( value ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * In R, single quotes and double quotes are interchangeable. Using single | ||
| + * quotes is simpler to code. | ||
| + * | ||
| + * @param value The text to convert into a valid quoted R string. | ||
| + * @return The quoted value with embedded quotes escaped as necessary. | ||
| + */ | ||
| + public static String escape( final String value ) { | ||
| + return '\'' + escape( value, '\'', "\\'" ) + '\''; | ||
| + } | ||
| + | ||
| + /** | ||
| + * TODO: Make generic method for replacing text. | ||
| + * | ||
| + * @param haystack Search this string for the needle, must not be null. | ||
| + * @param needle The character to find in the haystack. | ||
| + * @param thread Replace the needle with this text, if the needle is found. | ||
| + * @return The haystack with the all instances of needle replaced with thread. | ||
| + */ | ||
| + @SuppressWarnings( "SameParameterValue" ) | ||
| + private static String escape( | ||
| + final String haystack, final char needle, final String thread ) { | ||
| + assert haystack != null; | ||
| + assert thread != null; | ||
| + | ||
| + int end = haystack.indexOf( needle ); | ||
| + | ||
| + if( end < 0 ) { | ||
| + return haystack; | ||
| + } | ||
| + | ||
| + int start = 0; | ||
| + | ||
| + // Replace up to 32 occurrences before reallocating the internal buffer. | ||
| + final var sb = new StringBuilder( haystack.length() + 32 ); | ||
| + | ||
| + while( end >= 0 ) { | ||
| + sb.append( haystack, start, end ).append( thread ); | ||
| + start = end + 1; | ||
| + end = haystack.indexOf( needle, start ); | ||
| + } | ||
| + | ||
| + return sb.append( haystack.substring( start ) ).toString(); | ||
| + } | ||
| +} | ||
| +/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | ||
| +package com.keenwrite.processors.variable; | ||
| + | ||
| +import com.keenwrite.processors.ExecutorProcessor; | ||
| +import com.keenwrite.processors.Processor; | ||
| +import com.keenwrite.processors.ProcessorContext; | ||
| +import com.keenwrite.sigils.SigilKeyOperator; | ||
| + | ||
| +import java.util.HashMap; | ||
| +import java.util.Map; | ||
| +import java.util.function.Function; | ||
| + | ||
| +import static com.keenwrite.processors.text.TextReplacementFactory.replace; | ||
| + | ||
| +/** | ||
| + * Processes interpolated string definitions in the document and inserts | ||
| + * their values into the post-processed text. The default variable syntax is | ||
| + * <pre>{{variable}}</pre> (a.k.a., moustache syntax). | ||
| + */ | ||
| +public class VariableProcessor | ||
| + extends ExecutorProcessor<String> implements Function<String, String> { | ||
| + | ||
| + private final ProcessorContext mContext; | ||
| + private final SigilKeyOperator mSigilOperator; | ||
| + | ||
| + /** | ||
| + * Constructs a processor capable of interpolating string definitions. | ||
| + * | ||
| + * @param successor Subsequent link in the processing chain. | ||
| + * @param context Contains resolved definitions map. | ||
| + */ | ||
| + public VariableProcessor( | ||
| + final Processor<String> successor, | ||
| + final ProcessorContext context ) { | ||
| + super( successor ); | ||
| + | ||
| + mContext = context; | ||
| + mSigilOperator = createKeyOperator( context ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Subclasses may change the type of operation performed on keys, such as | ||
| + * wrapping key names in sigils. | ||
| + * | ||
| + * @param context Provides the name of the file being edited. | ||
| + * @return An operator for transforming key names. | ||
| + */ | ||
| + protected SigilKeyOperator createKeyOperator( | ||
| + final ProcessorContext context ) { | ||
| + return context.createKeyOperator(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Returns the map to use for variable substitution. | ||
| + * | ||
| + * @return A map of variable names to values, with keys wrapped in sigils. | ||
| + */ | ||
| + public Map<String, String> getDefinitions() { | ||
| + return entoken( mContext.getInterpolatedDefinitions() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Subclasses may override this method to change how keys are wrapped | ||
| + * in sigils. | ||
| + * | ||
| + * @param key The key to enwrap. | ||
| + * @return The wrapped key. | ||
| + */ | ||
| + protected String processKey( final String key ) { | ||
| + return mSigilOperator.apply( key ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Subclasses may override this method to modify values prior to use. This | ||
| + * can be used, for example, to escape values prior to evaluating by a | ||
| + * scripting engine. | ||
| + * | ||
| + * @param value The value to process. | ||
| + * @return The processed value. | ||
| + */ | ||
| + protected String processValue( final String value ) { | ||
| + return value; | ||
| + } | ||
| + | ||
| + /** | ||
| + * Answers whether the given key is wrapped in sigil tokens. | ||
| + * | ||
| + * @param key The key to analyze. | ||
| + * @return {@code true} if the key is wrapped in sigils. | ||
| + */ | ||
| + public boolean hasSigils( final String key ) { | ||
| + return mSigilOperator.match( key ).find(); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Processes the given text document by replacing variables with their values. | ||
| + * | ||
| + * @param text The document text that includes variables that should be | ||
| + * replaced with values when rendered as HTML. | ||
| + * @return The text with all variables replaced. | ||
| + */ | ||
| + @Override | ||
| + public String apply( final String text ) { | ||
| + assert text != null; | ||
| + | ||
| + return replace( text, getDefinitions() ); | ||
| + } | ||
| + | ||
| + /** | ||
| + * Converts the given map from regular variables to processor-specific | ||
| + * variables. | ||
| + * | ||
| + * @param map Map of variable names to values. | ||
| + * @return Map of variables with the keys and values subjected to | ||
| + * post-processing. | ||
| + */ | ||
| + protected Map<String, String> entoken( final Map<String, String> map ) { | ||
| + assert map != null; | ||
| + | ||
| + final var result = new HashMap<String, String>( map.size() ); | ||
| + | ||
| + map.forEach( ( k, v ) -> result.put( processKey( k ), processValue( v ) ) ); | ||
| + | ||
| + return result; | ||
| + } | ||
| +} | ||
| addAction( "file.export.pdf.repeat", _ -> actions.file_export_repeat() ), | ||
| addAction( "file.export.html.dir", _ -> actions.file_export_html_dir() ), | ||
| + addAction( "file.export.text_tex.dir", _ -> actions.file_export_text_tex_dir() ), | ||
| addAction( "file.export.html_svg", _ -> actions.file_export_html_svg() ), | ||
| addAction( "file.export.html_tex", _ -> actions.file_export_html_tex() ), | ||
| + addAction( "file.export.text_tex", _ -> actions.file_export_text_tex() ), | ||
| addAction( "file.export.xhtml_tex", _ -> actions.file_export_xhtml_tex() ) | ||
| ), |
| import static com.keenwrite.ExportFormat.*; | ||
| import static com.keenwrite.Messages.get; | ||
| -import static com.keenwrite.constants.Constants.PDF_DEFAULT; | ||
| import static com.keenwrite.constants.Constants.USER_DIRECTORY; | ||
| import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE; | ||
| : userHomeParent; | ||
| - final var filename = format.toExportFilename( editor.getPath() ); | ||
| - final var selected = PDF_DEFAULT | ||
| - .getName() | ||
| - .equals( exported.get().getName() ); | ||
| + final var filename = format.toExportFilename( exported.get() ); | ||
| + | ||
| final var selection = pickFile( | ||
| - selected | ||
| - ? filename | ||
| - : exported.get(), | ||
| + filename, | ||
| exportPath, | ||
| FILE_EXPORT | ||
| public void file_export_html_tex() { | ||
| file_export( HTML_TEX_DELIMITED ); | ||
| + } | ||
| + | ||
| + public void file_export_text_tex() { | ||
| + file_export( TEXT_TEX, false ); | ||
| + } | ||
| + | ||
| + public void file_export_text_tex_dir() { | ||
| + file_export( TEXT_TEX, true ); | ||
| } | ||
| @Override | ||
| public void setInitialDirectory( final Path path ) { | ||
| - final var file = toFile( path ); | ||
| + final var directory = toFile( path ); | ||
| mChooser.setInitialDirectory( | ||
| - file.exists() ? file : new File( getUserHome() ) | ||
| + directory.exists() ? directory : new File( getUserHome() ) | ||
| ); | ||
| } |
| Action.file.export.pdf.dir.icon=FILE_PDF_ALT | ||
| +Action.file.export.text_tex.dir.description=Convert files in document directory | ||
| +Action.file.export.text_tex.dir.text=Joined Text | ||
| +Action.file.export.text_tex.dir.icon=FILE_TEXT_ALT | ||
| + | ||
| Action.file.export.pdf.repeat.description=Repeat previous typesetting command | ||
| Action.file.export.pdf.repeat.accelerator=Ctrl+Shift+E | ||
| Action.file.export.html_tex.description=Export the current document as HTML + TeX | ||
| Action.file.export.html_tex.text=HTML and _TeX | ||
| + | ||
| +Action.file.export.text_tex.description=Export the current document as text + TeX | ||
| +Action.file.export.text_tex.text=Text and TeX | ||
| Action.file.export.xhtml_tex.description=Export as XHTML + TeX | ||
| Author | DaveJarvis <email> |
|---|---|
| Date | 2024-04-14 15:15:16 GMT-0700 |
| Commit | 3e8076c581cc9498c9bd46aba936f606009b53e6 |
| Parent | 0145f98 |
| Delta | 902 lines added, 789 lines removed, 113-line increase |