Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
bug-filter.xml
</Match>
- <Match class="com.keenwrite.processors.HtmlPreviewProcessor">
+ <Match class="com.keenwrite.processors.html.HtmlPreviewProcessor">
<Method name="&lt;init&gt;" />
<Bug code="ST" />
src/main/java/com/keenwrite/AppCommands.java
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;
src/main/java/com/keenwrite/ExportFormat.java
-/* 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" ),
/**
src/main/java/com/keenwrite/MainPane.java
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;
src/main/java/com/keenwrite/processors/HtmlPreviewProcessor.java
-/* 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;
- }
-}
src/main/java/com/keenwrite/processors/IdentityProcessor.java
-/* 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;
- }
-}
src/main/java/com/keenwrite/processors/PdfProcessor.java
-/* 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;
- }
-}
src/main/java/com/keenwrite/processors/PreformattedProcessor.java
-/* 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>";
- }
-}
src/main/java/com/keenwrite/processors/ProcessorContext.java
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;
src/main/java/com/keenwrite/processors/ProcessorFactory.java
-/* 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 );
}
src/main/java/com/keenwrite/processors/RBootstrapProcessor.java
-/* 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 );
- }
-}
src/main/java/com/keenwrite/processors/VariableProcessor.java
-/* 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;
- }
-}
src/main/java/com/keenwrite/processors/XhtmlProcessor.java
-/* 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();
- }
-}
src/main/java/com/keenwrite/processors/html/HtmlPreviewProcessor.java
+/* 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;
+ }
+}
src/main/java/com/keenwrite/processors/html/IdentityProcessor.java
+/* 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;
+ }
+}
src/main/java/com/keenwrite/processors/html/PreformattedProcessor.java
+/* 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>";
+ }
+}
src/main/java/com/keenwrite/processors/html/XhtmlProcessor.java
+/* 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();
+ }
+}
src/main/java/com/keenwrite/processors/markdown/BaseMarkdownProcessor.java
-/* 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;
src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
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;
/**
src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedBlockExtension.java
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;
src/main/java/com/keenwrite/processors/markdown/extensions/r/RInlineExtension.java
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;
src/main/java/com/keenwrite/processors/markdown/extensions/tex/TexNodeRenderer.java
HTML_TEX_DELIMITED, new TexDelimitedNodeRenderer(),
XHTML_TEX, new TexElementNodeRenderer( true ),
+ TEXT_TEX, new TexElementNodeRenderer( true ),
NONE, RENDERER
);
src/main/java/com/keenwrite/processors/pdf/PdfProcessor.java
+/* 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;
+ }
+}
src/main/java/com/keenwrite/processors/r/RBootstrapController.java
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;
src/main/java/com/keenwrite/processors/r/RBootstrapProcessor.java
+/* 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 );
+ }
+}
src/main/java/com/keenwrite/processors/r/RInlineEvaluator.java
import com.keenwrite.processors.Processor;
+import com.keenwrite.processors.variable.RVariableProcessor;
import java.util.function.Function;
src/main/java/com/keenwrite/processors/r/RVariableProcessor.java
-/* 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();
- }
-}
src/main/java/com/keenwrite/processors/text/TextProcessor.java
+/* 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 );
+ }
+}
src/main/java/com/keenwrite/processors/variable/RVariableProcessor.java
+/* 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();
+ }
+}
src/main/java/com/keenwrite/processors/variable/VariableProcessor.java
+/* 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;
+ }
+}
src/main/java/com/keenwrite/ui/actions/ApplicationBars.java
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() )
),
src/main/java/com/keenwrite/ui/actions/GuiCommands.java
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 );
}
src/main/java/com/keenwrite/ui/explorer/FilePickerFactory.java
@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() )
);
}
src/main/resources/com/keenwrite/messages.properties
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

Adds export as plain text

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