| 18 | 18 | Clone the repository as follows: |
| 19 | 19 | |
| 20 | git clone https://gitlab.com/DaveJarvis/KeenWrite.git | |
| 20 | git clone https://gitlab.com/DaveJarvis/KeenWrite.git keenwrite | |
| 21 | 21 | |
| 22 | 22 | The repository is cloned. |
| 12 | 12 | </Match> |
| 13 | 13 | |
| 14 | <Match class="com.keenwrite.processors.HtmlPreviewProcessor"> | |
| 14 | <Match class="com.keenwrite.processors.html.HtmlPreviewProcessor"> | |
| 15 | 15 | <Method name="<init>" /> |
| 16 | 16 | <Bug code="ST" /> |
| 10 | 10 | import com.keenwrite.processors.Processor; |
| 11 | 11 | import com.keenwrite.processors.ProcessorContext; |
| 12 | import com.keenwrite.processors.RBootstrapProcessor; | |
| 12 | import com.keenwrite.processors.r.RBootstrapProcessor; | |
| 13 | 13 | |
| 14 | 14 | import java.io.IOException; |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 1 | /* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * SPDX-License-Identifier: MIT | |
| 4 | */ | |
| 2 | 5 | package com.keenwrite; |
| 3 | 6 | |
| ... | ||
| 32 | 35 | * For XHTML exports, encode TeX using {@code $} delimiters. |
| 33 | 36 | */ |
| 34 | XHTML_TEX( ".xml" ), | |
| 37 | XHTML_TEX( ".xhtml" ), | |
| 38 | ||
| 39 | /** | |
| 40 | * For TEXT exports, encode TeX using {@code $} delimiters. | |
| 41 | */ | |
| 42 | TEXT_TEX( ".txt" ), | |
| 35 | 43 | |
| 36 | 44 | /** |
| 18 | 18 | import com.keenwrite.preferences.Workspace; |
| 19 | 19 | import com.keenwrite.preview.HtmlPreview; |
| 20 | import com.keenwrite.processors.HtmlPreviewProcessor; | |
| 20 | import com.keenwrite.processors.html.HtmlPreviewProcessor; | |
| 21 | 21 | import com.keenwrite.processors.Processor; |
| 22 | 22 | import com.keenwrite.processors.ProcessorContext; |
| ... | ||
| 77 | 77 | import static com.keenwrite.io.SysFile.toFile; |
| 78 | 78 | import static com.keenwrite.preferences.AppKeys.*; |
| 79 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | |
| 79 | import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | |
| 80 | 80 | import static com.keenwrite.processors.ProcessorContext.Mutator; |
| 81 | 81 | import static com.keenwrite.processors.ProcessorContext.builder; |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors; | |
| 3 | ||
| 4 | import com.keenwrite.preview.HtmlPreview; | |
| 5 | ||
| 6 | /** | |
| 7 | * Responsible for notifying the {@link HtmlPreview} when the succession | |
| 8 | * chain has updated. This decouples knowledge of changes to the editor panel | |
| 9 | * from the HTML preview panel as well as any processing that takes place | |
| 10 | * before the final HTML preview is rendered. This is the last link in the | |
| 11 | * processor chain. | |
| 12 | */ | |
| 13 | public final class HtmlPreviewProcessor extends ExecutorProcessor<String> { | |
| 14 | /** | |
| 15 | * There is only one preview panel. | |
| 16 | */ | |
| 17 | private static HtmlPreview sHtmlPreview; | |
| 18 | ||
| 19 | /** | |
| 20 | * Constructs the end of a processing chain. | |
| 21 | * | |
| 22 | * @param htmlPreview The pane to update with the post-processed document. | |
| 23 | */ | |
| 24 | public HtmlPreviewProcessor( final HtmlPreview htmlPreview ) { | |
| 25 | sHtmlPreview = htmlPreview; | |
| 26 | } | |
| 27 | ||
| 28 | /** | |
| 29 | * Update the preview panel using HTML from the succession chain. | |
| 30 | * | |
| 31 | * @param html The document content to render in the preview pane. The HTML | |
| 32 | * should not contain a doctype, head, or body tag. | |
| 33 | * @return The given {@code html} string. | |
| 34 | */ | |
| 35 | @Override | |
| 36 | public String apply( final String html ) { | |
| 37 | assert html != null; | |
| 38 | ||
| 39 | sHtmlPreview.render( html ); | |
| 40 | return html; | |
| 41 | } | |
| 42 | } | |
| 43 | 1 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors; | |
| 3 | ||
| 4 | /** | |
| 5 | * Responsible for transforming a string into itself. This is used at the | |
| 6 | * end of a processing chain when no more processing is required. | |
| 7 | */ | |
| 8 | public final class IdentityProcessor extends ExecutorProcessor<String> { | |
| 9 | public static final IdentityProcessor IDENTITY = new IdentityProcessor(); | |
| 10 | ||
| 11 | /** | |
| 12 | * Constructs a new instance having no successor (the default successor is | |
| 13 | * {@code null}). | |
| 14 | */ | |
| 15 | private IdentityProcessor() { | |
| 16 | } | |
| 17 | ||
| 18 | /** | |
| 19 | * Returns the given string without modification. | |
| 20 | * | |
| 21 | * @param s The string to return. | |
| 22 | * @return The value of s. | |
| 23 | */ | |
| 24 | @Override | |
| 25 | public String apply( final String s ) { | |
| 26 | return s; | |
| 27 | } | |
| 28 | } | |
| 29 | 1 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors; | |
| 3 | ||
| 4 | import com.keenwrite.typesetting.Typesetter; | |
| 5 | ||
| 6 | import static com.keenwrite.Bootstrap.APP_TITLE_ABBR; | |
| 7 | import static com.keenwrite.events.StatusEvent.clue; | |
| 8 | import static com.keenwrite.io.MediaType.TEXT_XML; | |
| 9 | import static com.keenwrite.io.SysFile.normalize; | |
| 10 | import static com.keenwrite.typesetting.Typesetter.Mutator; | |
| 11 | import static com.keenwrite.util.Strings.sanitize; | |
| 12 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 13 | import static java.nio.file.Files.deleteIfExists; | |
| 14 | import static java.nio.file.Files.writeString; | |
| 15 | ||
| 16 | /** | |
| 17 | * Responsible for using a typesetting engine to convert an XHTML document | |
| 18 | * into a PDF file. This must not be run from the JavaFX thread. | |
| 19 | */ | |
| 20 | public final class PdfProcessor extends ExecutorProcessor<String> { | |
| 21 | private final ProcessorContext mProcessorContext; | |
| 22 | ||
| 23 | public PdfProcessor( final ProcessorContext context ) { | |
| 24 | assert context != null; | |
| 25 | mProcessorContext = context; | |
| 26 | } | |
| 27 | ||
| 28 | /** | |
| 29 | * Converts a document by calling a third-party application to typeset the | |
| 30 | * given XHTML document. | |
| 31 | * | |
| 32 | * @param xhtml The document to convert to a PDF file. | |
| 33 | * @return {@code null} because there is no valid return value from generating | |
| 34 | * a PDF file. | |
| 35 | */ | |
| 36 | public String apply( final String xhtml ) { | |
| 37 | try { | |
| 38 | clue( "Main.status.typeset.create" ); | |
| 39 | ||
| 40 | final var context = mProcessorContext; | |
| 41 | final var targetPath = context.getTargetPath(); | |
| 42 | clue( "Main.status.typeset.setting", "target", targetPath ); | |
| 43 | ||
| 44 | final var parent = normalize( targetPath.toAbsolutePath().getParent() ); | |
| 45 | ||
| 46 | final var document = TEXT_XML.createTempFile( APP_TITLE_ABBR, parent ); | |
| 47 | final var sourcePath = writeString( document, xhtml, UTF_8 ); | |
| 48 | clue( "Main.status.typeset.setting", "source", sourcePath ); | |
| 49 | ||
| 50 | final var themeDir = normalize( context.getThemeDir() ); | |
| 51 | clue( "Main.status.typeset.setting", "themes", themeDir ); | |
| 52 | ||
| 53 | final var imageDir = normalize( context.getImageDir() ); | |
| 54 | clue( "Main.status.typeset.setting", "images", imageDir ); | |
| 55 | ||
| 56 | final var imageOrder = context.getImageOrder(); | |
| 57 | clue( "Main.status.typeset.setting", "order", imageOrder ); | |
| 58 | ||
| 59 | final var cacheDir = normalize( context.getCacheDir() ); | |
| 60 | clue( "Main.status.typeset.setting", "caches", cacheDir ); | |
| 61 | ||
| 62 | final var fontDir = normalize( context.getFontDir() ); | |
| 63 | clue( "Main.status.typeset.setting", "fonts", fontDir ); | |
| 64 | ||
| 65 | final var rWorkDir = normalize( context.getRWorkingDir() ); | |
| 66 | clue( "Main.status.typeset.setting", "r-work", rWorkDir ); | |
| 67 | ||
| 68 | final var modesEnabled = sanitize( context.getModesEnabled() ); | |
| 69 | clue( "Main.status.typeset.setting", "mode", modesEnabled ); | |
| 70 | ||
| 71 | final var autoRemove = context.getAutoRemove(); | |
| 72 | clue( "Main.status.typeset.setting", "purge", autoRemove ); | |
| 73 | ||
| 74 | final var typesetter = Typesetter | |
| 75 | .builder() | |
| 76 | .with( Mutator::setTargetPath, targetPath ) | |
| 77 | .with( Mutator::setSourcePath, sourcePath ) | |
| 78 | .with( Mutator::setThemeDir, themeDir ) | |
| 79 | .with( Mutator::setImageDir, imageDir ) | |
| 80 | .with( Mutator::setCacheDir, cacheDir ) | |
| 81 | .with( Mutator::setFontDir, fontDir ) | |
| 82 | .with( Mutator::setModesEnabled, modesEnabled ) | |
| 83 | .with( Mutator::setAutoRemove, autoRemove ) | |
| 84 | .build(); | |
| 85 | ||
| 86 | try { | |
| 87 | typesetter.typeset(); | |
| 88 | } | |
| 89 | finally { | |
| 90 | // Smote the temporary file after typesetting the document. | |
| 91 | if( typesetter.autoRemove() ) { | |
| 92 | deleteIfExists( document ); | |
| 93 | } | |
| 94 | } | |
| 95 | } catch( final Exception ex ) { | |
| 96 | // Typesetter runtime exceptions will pass up the call stack. | |
| 97 | clue( "Main.status.typeset.failed", ex ); | |
| 98 | } | |
| 99 | ||
| 100 | // Do not continue processing (the document was typeset into a binary). | |
| 101 | return null; | |
| 102 | } | |
| 103 | } | |
| 104 | 1 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors; | |
| 3 | ||
| 4 | /** | |
| 5 | * This is the default processor used when an unknown file name extension is | |
| 6 | * encountered. It processes the text by enclosing it in an HTML {@code <pre>} | |
| 7 | * element. | |
| 8 | */ | |
| 9 | public final class PreformattedProcessor extends ExecutorProcessor<String> { | |
| 10 | ||
| 11 | /** | |
| 12 | * Passes the link to the super constructor. | |
| 13 | * | |
| 14 | * @param successor The next processor in the chain to use for text | |
| 15 | * processing. | |
| 16 | */ | |
| 17 | public PreformattedProcessor( final Processor<String> successor ) { | |
| 18 | super( successor ); | |
| 19 | } | |
| 20 | ||
| 21 | /** | |
| 22 | * Returns the given string, modified with "pre" tags. | |
| 23 | * | |
| 24 | * @param t The string to return, enclosed in "pre" tags. | |
| 25 | * @return The value of t wrapped in "pre" tags. | |
| 26 | */ | |
| 27 | @Override | |
| 28 | public String apply( final String t ) { | |
| 29 | return "<pre>" + t + "</pre>"; | |
| 30 | } | |
| 31 | } | |
| 32 | 1 |
| 12 | 12 | import com.keenwrite.io.MediaType; |
| 13 | 13 | import com.keenwrite.io.MediaTypeExtension; |
| 14 | import com.keenwrite.processors.variable.VariableProcessor; | |
| 14 | 15 | import com.keenwrite.sigils.PropertyKeyOperator; |
| 15 | 16 | import com.keenwrite.sigils.SigilKeyOperator; |
| ... | ||
| 32 | 33 | import static com.keenwrite.io.SysFile.toFile; |
| 33 | 34 | import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate; |
| 34 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | |
| 35 | import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | |
| 35 | 36 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; |
| 36 | 37 | import static com.keenwrite.util.Strings.sanitize; |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 1 | /* Copyright 2020-2024 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * SPDX-License-Identifier: MIT | |
| 4 | */ | |
| 2 | 5 | package com.keenwrite.processors; |
| 3 | 6 | |
| 7 | import com.keenwrite.processors.html.PreformattedProcessor; | |
| 8 | import com.keenwrite.processors.html.XhtmlProcessor; | |
| 4 | 9 | import com.keenwrite.processors.markdown.MarkdownProcessor; |
| 10 | import com.keenwrite.processors.pdf.PdfProcessor; | |
| 11 | import com.keenwrite.processors.text.TextProcessor; | |
| 12 | import com.keenwrite.processors.variable.VariableProcessor; | |
| 5 | 13 | |
| 14 | import static com.keenwrite.ExportFormat.TEXT_TEX; | |
| 6 | 15 | import static com.keenwrite.io.FileType.RMARKDOWN; |
| 7 | 16 | import static com.keenwrite.io.FileType.SOURCE; |
| 8 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | |
| 17 | import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | |
| 9 | 18 | |
| 10 | 19 | /** |
| ... | ||
| 58 | 67 | case NONE -> preview; |
| 59 | 68 | case XHTML_TEX -> createXhtmlProcessor( context ); |
| 69 | case TEXT_TEX -> createTextProcessor( context ); | |
| 60 | 70 | case APPLICATION_PDF -> createPdfProcessor( context ); |
| 61 | 71 | default -> createIdentityProcessor( context ); |
| 62 | 72 | }; |
| 63 | 73 | |
| 64 | 74 | final var inputType = context.getSourceFileType(); |
| 65 | 75 | final Processor<String> processor; |
| 66 | 76 | |
| 67 | // When there's no preview, convert to HTML. | |
| 68 | 77 | if( preview == null ) { |
| 69 | processor = createMarkdownProcessor( successor, context ); | |
| 78 | if( outputType == TEXT_TEX ) { | |
| 79 | processor = successor; | |
| 80 | } | |
| 81 | else { | |
| 82 | processor = createMarkdownProcessor( successor, context ); | |
| 83 | } | |
| 70 | 84 | } |
| 71 | 85 | else { |
| ... | ||
| 121 | 135 | final ProcessorContext context ) { |
| 122 | 136 | return createXhtmlProcessor( IDENTITY, context ); |
| 137 | } | |
| 138 | ||
| 139 | private static Processor<String> createTextProcessor( | |
| 140 | final ProcessorContext context ) { | |
| 141 | return new TextProcessor( IDENTITY, context ); | |
| 123 | 142 | } |
| 124 | 143 | |
| 1 | /* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * SPDX-License-Identifier: MIT | |
| 4 | */ | |
| 5 | package com.keenwrite.processors; | |
| 6 | ||
| 7 | import com.keenwrite.processors.r.RBootstrapController; | |
| 8 | ||
| 9 | public class RBootstrapProcessor extends ExecutorProcessor<String> { | |
| 10 | private final Processor<String> mSuccessor; | |
| 11 | private final ProcessorContext mContext; | |
| 12 | ||
| 13 | public RBootstrapProcessor( | |
| 14 | final Processor<String> successor, | |
| 15 | final ProcessorContext context ) { | |
| 16 | assert successor != null; | |
| 17 | assert context != null; | |
| 18 | ||
| 19 | mSuccessor = successor; | |
| 20 | mContext = context; | |
| 21 | } | |
| 22 | ||
| 23 | /** | |
| 24 | * Processes the given text document by replacing variables with their values. | |
| 25 | * | |
| 26 | * @param text The document text that includes variables that should be | |
| 27 | * replaced with values when rendered as HTML. | |
| 28 | * @return The text with all variables replaced. | |
| 29 | */ | |
| 30 | @Override | |
| 31 | public String apply( final String text ) { | |
| 32 | assert text != null; | |
| 33 | ||
| 34 | final var bootstrap = mContext.getRScript(); | |
| 35 | final var workingDir = mContext.getRWorkingDir().toString(); | |
| 36 | final var definitions = mContext.getDefinitions(); | |
| 37 | ||
| 38 | RBootstrapController.update( bootstrap, workingDir, definitions ); | |
| 39 | ||
| 40 | return mSuccessor.apply( text ); | |
| 41 | } | |
| 42 | } | |
| 43 | 1 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors; | |
| 3 | ||
| 4 | import com.keenwrite.sigils.SigilKeyOperator; | |
| 5 | ||
| 6 | import java.util.HashMap; | |
| 7 | import java.util.Map; | |
| 8 | import java.util.function.Function; | |
| 9 | ||
| 10 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; | |
| 11 | ||
| 12 | /** | |
| 13 | * Processes interpolated string definitions in the document and inserts | |
| 14 | * their values into the post-processed text. The default variable syntax is | |
| 15 | * <pre>{{variable}}</pre> (a.k.a., moustache syntax). | |
| 16 | */ | |
| 17 | public class VariableProcessor | |
| 18 | extends ExecutorProcessor<String> implements Function<String, String> { | |
| 19 | ||
| 20 | private final ProcessorContext mContext; | |
| 21 | private final SigilKeyOperator mSigilOperator; | |
| 22 | ||
| 23 | /** | |
| 24 | * Constructs a processor capable of interpolating string definitions. | |
| 25 | * | |
| 26 | * @param successor Subsequent link in the processing chain. | |
| 27 | * @param context Contains resolved definitions map. | |
| 28 | */ | |
| 29 | public VariableProcessor( | |
| 30 | final Processor<String> successor, | |
| 31 | final ProcessorContext context ) { | |
| 32 | super( successor ); | |
| 33 | ||
| 34 | mContext = context; | |
| 35 | mSigilOperator = createKeyOperator( context ); | |
| 36 | } | |
| 37 | ||
| 38 | /** | |
| 39 | * Subclasses may change the type of operation performed on keys, such as | |
| 40 | * wrapping key names in sigils. | |
| 41 | * | |
| 42 | * @param context Provides the name of the file being edited. | |
| 43 | * @return An operator for transforming key names. | |
| 44 | */ | |
| 45 | protected SigilKeyOperator createKeyOperator( | |
| 46 | final ProcessorContext context ) { | |
| 47 | return context.createKeyOperator(); | |
| 48 | } | |
| 49 | ||
| 50 | /** | |
| 51 | * Returns the map to use for variable substitution. | |
| 52 | * | |
| 53 | * @return A map of variable names to values, with keys wrapped in sigils. | |
| 54 | */ | |
| 55 | protected Map<String, String> getDefinitions() { | |
| 56 | return entoken( mContext.getInterpolatedDefinitions() ); | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * Subclasses may override this method to change how keys are wrapped | |
| 61 | * in sigils. | |
| 62 | * | |
| 63 | * @param key The key to enwrap. | |
| 64 | * @return The wrapped key. | |
| 65 | */ | |
| 66 | protected String processKey( final String key ) { | |
| 67 | return mSigilOperator.apply( key ); | |
| 68 | } | |
| 69 | ||
| 70 | /** | |
| 71 | * Subclasses may override this method to modify values prior to use. This | |
| 72 | * can be used, for example, to escape values prior to evaluating by a | |
| 73 | * scripting engine. | |
| 74 | * | |
| 75 | * @param value The value to process. | |
| 76 | * @return The processed value. | |
| 77 | */ | |
| 78 | protected String processValue( final String value ) { | |
| 79 | return value; | |
| 80 | } | |
| 81 | ||
| 82 | /** | |
| 83 | * Answers whether the given key is wrapped in sigil tokens. | |
| 84 | * | |
| 85 | * @param key The key to analyze. | |
| 86 | * @return {@code true} if the key is wrapped in sigils. | |
| 87 | */ | |
| 88 | public boolean hasSigils( final String key ) { | |
| 89 | return mSigilOperator.match( key ).find(); | |
| 90 | } | |
| 91 | ||
| 92 | /** | |
| 93 | * Processes the given text document by replacing variables with their values. | |
| 94 | * | |
| 95 | * @param text The document text that includes variables that should be | |
| 96 | * replaced with values when rendered as HTML. | |
| 97 | * @return The text with all variables replaced. | |
| 98 | */ | |
| 99 | @Override | |
| 100 | public String apply( final String text ) { | |
| 101 | assert text != null; | |
| 102 | ||
| 103 | return replace( text, getDefinitions() ); | |
| 104 | } | |
| 105 | ||
| 106 | /** | |
| 107 | * Converts the given map from regular variables to processor-specific | |
| 108 | * variables. | |
| 109 | * | |
| 110 | * @param map Map of variable names to values. | |
| 111 | * @return Map of variables with the keys and values subjected to | |
| 112 | * post-processing. | |
| 113 | */ | |
| 114 | protected Map<String, String> entoken( final Map<String, String> map ) { | |
| 115 | assert map != null; | |
| 116 | ||
| 117 | final var result = new HashMap<String, String>( map.size() ); | |
| 118 | ||
| 119 | map.forEach( ( k, v ) -> result.put( processKey( k ), processValue( v ) ) ); | |
| 120 | ||
| 121 | return result; | |
| 122 | } | |
| 123 | } | |
| 124 | 1 |
| 1 | /* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * SPDX-License-Identifier: MIT | |
| 4 | */ | |
| 5 | package com.keenwrite.processors; | |
| 6 | ||
| 7 | import com.keenwrite.dom.DocumentParser; | |
| 8 | import com.keenwrite.io.MediaTypeExtension; | |
| 9 | import com.keenwrite.ui.heuristics.WordCounter; | |
| 10 | import com.keenwrite.util.DataTypeConverter; | |
| 11 | import com.whitemagicsoftware.keenquotes.parser.Contractions; | |
| 12 | import com.whitemagicsoftware.keenquotes.parser.Curler; | |
| 13 | import org.w3c.dom.Document; | |
| 14 | ||
| 15 | import java.io.File; | |
| 16 | import java.io.FileNotFoundException; | |
| 17 | import java.nio.file.Path; | |
| 18 | import java.util.*; | |
| 19 | ||
| 20 | import static com.keenwrite.Bootstrap.APP_TITLE_ABBR; | |
| 21 | import static com.keenwrite.dom.DocumentParser.*; | |
| 22 | import static com.keenwrite.events.StatusEvent.clue; | |
| 23 | import static com.keenwrite.io.SysFile.toFile; | |
| 24 | import static com.keenwrite.io.downloads.DownloadManager.open; | |
| 25 | import static com.keenwrite.util.ProtocolScheme.getProtocol; | |
| 26 | import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML; | |
| 27 | import static java.lang.String.format; | |
| 28 | import static java.lang.String.valueOf; | |
| 29 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 30 | import static java.nio.file.Files.copy; | |
| 31 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; | |
| 32 | ||
| 33 | /** | |
| 34 | * Responsible for making an XHTML document complete by wrapping it with html | |
| 35 | * and body elements. This doesn't have to be super-efficient because it's | |
| 36 | * not run in real-time. | |
| 37 | */ | |
| 38 | public final class XhtmlProcessor extends ExecutorProcessor<String> { | |
| 39 | private static final Curler sTypographer = | |
| 40 | new Curler( createContractions(), FILTER_XML, true ); | |
| 41 | ||
| 42 | private final ProcessorContext mContext; | |
| 43 | ||
| 44 | public XhtmlProcessor( | |
| 45 | final Processor<String> successor, final ProcessorContext context ) { | |
| 46 | super( successor ); | |
| 47 | ||
| 48 | assert context != null; | |
| 49 | mContext = context; | |
| 50 | } | |
| 51 | ||
| 52 | /** | |
| 53 | * Responsible for producing a well-formed XML document complete with | |
| 54 | * metadata (title, author, keywords, copyright, and date). | |
| 55 | * | |
| 56 | * @param html The HTML document to transform into an XHTML document. | |
| 57 | * @return The transformed HTML document. | |
| 58 | */ | |
| 59 | @Override | |
| 60 | public String apply( final String html ) { | |
| 61 | clue( "Main.status.typeset.xhtml" ); | |
| 62 | ||
| 63 | try { | |
| 64 | final var doc = parse( html ); | |
| 65 | setMetaData( doc ); | |
| 66 | ||
| 67 | visit( doc, "//img", node -> { | |
| 68 | try { | |
| 69 | final var attrs = node.getAttributes(); | |
| 70 | final var attr = attrs.getNamedItem( "src" ); | |
| 71 | ||
| 72 | if( attr != null ) { | |
| 73 | final var src = attr.getTextContent(); | |
| 74 | final Path location; | |
| 75 | final Path imagesDir; | |
| 76 | ||
| 77 | // Download into a cache directory, which can be written to without | |
| 78 | // any possibility of overwriting local image files. Further, the | |
| 79 | // filenames are hashed as a second layer of protection. | |
| 80 | if( getProtocol( src ).isRemote() ) { | |
| 81 | location = downloadImage( src ); | |
| 82 | imagesDir = getCachesPath(); | |
| 83 | } | |
| 84 | else { | |
| 85 | location = resolveImage( src ); | |
| 86 | imagesDir = getImagesPath(); | |
| 87 | } | |
| 88 | ||
| 89 | final var relative = imagesDir.relativize( location ); | |
| 90 | ||
| 91 | attr.setTextContent( relative.toString() ); | |
| 92 | } | |
| 93 | } catch( final Exception ex ) { | |
| 94 | clue( ex ); | |
| 95 | } | |
| 96 | } ); | |
| 97 | ||
| 98 | final var document = DocumentParser.toString( doc ); | |
| 99 | final var curl = mContext.getCurlQuotes(); | |
| 100 | ||
| 101 | return curl ? sTypographer.apply( document ) : document; | |
| 102 | } catch( final Exception ex ) { | |
| 103 | clue( ex ); | |
| 104 | } | |
| 105 | ||
| 106 | return html; | |
| 107 | } | |
| 108 | ||
| 109 | /** | |
| 110 | * Applies the metadata fields to the document. | |
| 111 | * | |
| 112 | * @param doc The document to adorn with metadata. | |
| 113 | */ | |
| 114 | private void setMetaData( final Document doc ) { | |
| 115 | final var metadata = createMetaDataMap( doc ); | |
| 116 | final var title = metadata.get( "title" ); | |
| 117 | ||
| 118 | visit( doc, "/html/head", node -> { | |
| 119 | // Insert <title>text</title> inside <head>. | |
| 120 | node.appendChild( createElement( doc, "title", title ) ); | |
| 121 | // Insert <meta charset="utf-8"> inside <head>. | |
| 122 | node.appendChild( createEncoding( doc, UTF_8.toString() ) ); | |
| 123 | ||
| 124 | // Insert each <meta name=x content=y /> inside <head>. | |
| 125 | metadata.entrySet().forEach( | |
| 126 | entry -> node.appendChild( createMeta( doc, entry ) ) | |
| 127 | ); | |
| 128 | } ); | |
| 129 | } | |
| 130 | ||
| 131 | /** | |
| 132 | * Generates document metadata, including word count. | |
| 133 | * | |
| 134 | * @param doc The document containing the text to tally. | |
| 135 | * @return A map of metadata key/value pairs. | |
| 136 | */ | |
| 137 | private Map<String, String> createMetaDataMap( final Document doc ) { | |
| 138 | final var result = new LinkedHashMap<String, String>(); | |
| 139 | final var map = mContext.getInterpolatedDefinitions(); | |
| 140 | final var metadata = getMetadata(); | |
| 141 | ||
| 142 | metadata.forEach( | |
| 143 | ( key, value ) -> { | |
| 144 | final var interpolated = map.interpolate( value ); | |
| 145 | ||
| 146 | if( !interpolated.isEmpty() ) { | |
| 147 | result.put( key, interpolated ); | |
| 148 | } | |
| 149 | } | |
| 150 | ); | |
| 151 | result.put( "count", wordCount( doc ) ); | |
| 152 | ||
| 153 | return result; | |
| 154 | } | |
| 155 | ||
| 156 | /** | |
| 157 | * The metadata is in list form because the user interface for entering the | |
| 158 | * key-value pairs is a table, which requires a generic {@link List} rather | |
| 159 | * than a generic {@link Map}. | |
| 160 | * | |
| 161 | * @return The document metadata. | |
| 162 | */ | |
| 163 | private Map<String, String> getMetadata() { | |
| 164 | final var result = mContext.getMetadata(); | |
| 165 | return result == null ? new HashMap<>() : result; | |
| 166 | } | |
| 167 | ||
| 168 | /** | |
| 169 | * Hashes the URL so that the number of files doesn't eat up disk space | |
| 170 | * over time. For static resources, a feature could be added to prevent | |
| 171 | * downloading the URL if the hashed filename already exists. | |
| 172 | * | |
| 173 | * @param src The source file's URL to download. | |
| 174 | * @return A {@link Path} to the local file containing the URL's contents. | |
| 175 | * @throws Exception Could not download or save the file. | |
| 176 | */ | |
| 177 | private Path downloadImage( final String src ) throws Exception { | |
| 178 | final Path imagePath; | |
| 179 | final File imageFile; | |
| 180 | final var cachesPath = getCachesPath(); | |
| 181 | ||
| 182 | clue( "Main.status.image.xhtml.image.download", src ); | |
| 183 | ||
| 184 | try( final var response = open( src ) ) { | |
| 185 | final var mediaType = response.getMediaType(); | |
| 186 | ||
| 187 | final var ext = MediaTypeExtension.valueFrom( mediaType ).getExtension(); | |
| 188 | final var hash = DataTypeConverter.toHex( DataTypeConverter.hash( src ) ); | |
| 189 | final var id = hash.toLowerCase(); | |
| 190 | ||
| 191 | imagePath = cachesPath.resolve( APP_TITLE_ABBR + id + '.' + ext ); | |
| 192 | imageFile = toFile( imagePath ); | |
| 193 | ||
| 194 | // Preserve image files if auto-remove is turned off. | |
| 195 | if( autoRemove() ) { | |
| 196 | imageFile.deleteOnExit(); | |
| 197 | } | |
| 198 | ||
| 199 | try( final var image = response.getInputStream() ) { | |
| 200 | copy( image, imagePath, REPLACE_EXISTING ); | |
| 201 | } | |
| 202 | ||
| 203 | if( mediaType.isSvg() ) { | |
| 204 | sanitize( imagePath ); | |
| 205 | } | |
| 206 | } | |
| 207 | ||
| 208 | final var key = imageFile.exists() | |
| 209 | ? "Main.status.image.xhtml.image.saved" | |
| 210 | : "Main.status.image.xhtml.image.failed"; | |
| 211 | clue( key, imageFile ); | |
| 212 | ||
| 213 | return imagePath; | |
| 214 | } | |
| 215 | ||
| 216 | private Path resolveImage( final String src ) throws Exception { | |
| 217 | var imagePath = getImagesPath(); | |
| 218 | var found = false; | |
| 219 | ||
| 220 | Path imageFile = null; | |
| 221 | ||
| 222 | clue( "Main.status.image.xhtml.image.resolve", src ); | |
| 223 | ||
| 224 | for( final var extension : getImageOrder() ) { | |
| 225 | final var filename = format( | |
| 226 | "%s%s%s", src, extension.isBlank() ? "" : ".", extension ); | |
| 227 | imageFile = imagePath.resolve( filename ); | |
| 228 | ||
| 229 | if( toFile( imageFile ).exists() ) { | |
| 230 | found = true; | |
| 231 | break; | |
| 232 | } | |
| 233 | } | |
| 234 | ||
| 235 | if( !found ) { | |
| 236 | imagePath = getDocumentDir(); | |
| 237 | imageFile = imagePath.resolve( src ); | |
| 238 | ||
| 239 | if( !toFile( imageFile ).exists() ) { | |
| 240 | final var filename = imageFile.toString(); | |
| 241 | clue( "Main.status.image.xhtml.image.missing", filename ); | |
| 242 | ||
| 243 | throw new FileNotFoundException( filename ); | |
| 244 | } | |
| 245 | } | |
| 246 | ||
| 247 | clue( "Main.status.image.xhtml.image.found", imageFile.toString() ); | |
| 248 | ||
| 249 | return imageFile; | |
| 250 | } | |
| 251 | ||
| 252 | private Path getImagesPath() { | |
| 253 | return mContext.getImageDir(); | |
| 254 | } | |
| 255 | ||
| 256 | private Path getCachesPath() { | |
| 257 | return mContext.getCacheDir(); | |
| 258 | } | |
| 259 | ||
| 260 | /** | |
| 261 | * By including an "empty" extension, the first element returned | |
| 262 | * will be the empty string. Thus, the first extension to try is the | |
| 263 | * file's default extension. Subsequent iterations will try to find | |
| 264 | * a file that has a name matching one of the preferred extensions. | |
| 265 | * | |
| 266 | * @return A list of extensions, including an empty string at the start. | |
| 267 | */ | |
| 268 | private Iterable<String> getImageOrder() { | |
| 269 | return mContext.getImageOrder(); | |
| 270 | } | |
| 271 | ||
| 272 | /** | |
| 273 | * Returns the absolute path to the document being edited, which can be used | |
| 274 | * to find files included using relative paths. | |
| 275 | * | |
| 276 | * @return The directory containing the edited file. | |
| 277 | */ | |
| 278 | private Path getDocumentDir() { | |
| 279 | return mContext.getBaseDir(); | |
| 280 | } | |
| 281 | ||
| 282 | private Locale getLocale() { | |
| 283 | return mContext.getLocale(); | |
| 284 | } | |
| 285 | ||
| 286 | private boolean autoRemove() { | |
| 287 | return mContext.getAutoRemove(); | |
| 288 | } | |
| 289 | ||
| 290 | private String wordCount( final Document doc ) { | |
| 291 | final var sb = new StringBuilder( 65536 * 10 ); | |
| 292 | ||
| 293 | visit( | |
| 294 | doc, | |
| 295 | "//*[normalize-space( text() ) != '']", | |
| 296 | node -> sb.append( node.getTextContent() ) | |
| 297 | ); | |
| 298 | ||
| 299 | return valueOf( WordCounter.create( getLocale() ).count( sb.toString() ) ); | |
| 300 | } | |
| 301 | ||
| 302 | /** | |
| 303 | * Creates contracts with a custom set of unambiguous strings. | |
| 304 | * | |
| 305 | * @return List of contractions to use for curling straight quotes. | |
| 306 | */ | |
| 307 | private static Contractions createContractions() { | |
| 308 | return new Contractions.Builder().build(); | |
| 309 | } | |
| 310 | } | |
| 311 | 1 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.html; | |
| 3 | ||
| 4 | import com.keenwrite.preview.HtmlPreview; | |
| 5 | import com.keenwrite.processors.ExecutorProcessor; | |
| 6 | ||
| 7 | /** | |
| 8 | * Responsible for notifying the {@link HtmlPreview} when the succession | |
| 9 | * chain has updated. This decouples knowledge of changes to the editor panel | |
| 10 | * from the HTML preview panel as well as any processing that takes place | |
| 11 | * before the final HTML preview is rendered. This is the last link in the | |
| 12 | * processor chain. | |
| 13 | */ | |
| 14 | public final class HtmlPreviewProcessor extends ExecutorProcessor<String> { | |
| 15 | /** | |
| 16 | * There is only one preview panel. | |
| 17 | */ | |
| 18 | private static HtmlPreview sHtmlPreview; | |
| 19 | ||
| 20 | /** | |
| 21 | * Constructs the end of a processing chain. | |
| 22 | * | |
| 23 | * @param htmlPreview The pane to update with the post-processed document. | |
| 24 | */ | |
| 25 | public HtmlPreviewProcessor( final HtmlPreview htmlPreview ) { | |
| 26 | sHtmlPreview = htmlPreview; | |
| 27 | } | |
| 28 | ||
| 29 | /** | |
| 30 | * Update the preview panel using HTML from the succession chain. | |
| 31 | * | |
| 32 | * @param html The document content to render in the preview pane. The HTML | |
| 33 | * should not contain a doctype, head, or body tag. | |
| 34 | * @return The given {@code html} string. | |
| 35 | */ | |
| 36 | @Override | |
| 37 | public String apply( final String html ) { | |
| 38 | assert html != null; | |
| 39 | ||
| 40 | sHtmlPreview.render( html ); | |
| 41 | return html; | |
| 42 | } | |
| 43 | } | |
| 1 | 44 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.html; | |
| 3 | ||
| 4 | import com.keenwrite.processors.ExecutorProcessor; | |
| 5 | ||
| 6 | /** | |
| 7 | * Responsible for transforming a string into itself. This is used at the | |
| 8 | * end of a processing chain when no more processing is required. | |
| 9 | */ | |
| 10 | public final class IdentityProcessor extends ExecutorProcessor<String> { | |
| 11 | public static final IdentityProcessor IDENTITY = new IdentityProcessor(); | |
| 12 | ||
| 13 | /** | |
| 14 | * Constructs a new instance having no successor (the default successor is | |
| 15 | * {@code null}). | |
| 16 | */ | |
| 17 | private IdentityProcessor() { | |
| 18 | } | |
| 19 | ||
| 20 | /** | |
| 21 | * Returns the given string without modification. | |
| 22 | * | |
| 23 | * @param s The string to return. | |
| 24 | * @return The value of s. | |
| 25 | */ | |
| 26 | @Override | |
| 27 | public String apply( final String s ) { | |
| 28 | return s; | |
| 29 | } | |
| 30 | } | |
| 1 | 31 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * SPDX-License-Identifier: MIT | |
| 4 | */ | |
| 5 | package com.keenwrite.processors.html; | |
| 6 | ||
| 7 | import com.keenwrite.processors.ExecutorProcessor; | |
| 8 | import com.keenwrite.processors.Processor; | |
| 9 | ||
| 10 | /** | |
| 11 | * This is the default processor used when an unknown file name extension is | |
| 12 | * encountered. It processes the text by enclosing it in an HTML {@code <pre>} | |
| 13 | * element. | |
| 14 | */ | |
| 15 | public final class PreformattedProcessor extends ExecutorProcessor<String> { | |
| 16 | ||
| 17 | /** | |
| 18 | * Passes the link to the super constructor. | |
| 19 | * | |
| 20 | * @param successor The next processor in the chain to use for text | |
| 21 | * processing. | |
| 22 | */ | |
| 23 | public PreformattedProcessor( final Processor<String> successor ) { | |
| 24 | super( successor ); | |
| 25 | } | |
| 26 | ||
| 27 | /** | |
| 28 | * Returns the given string, modified with "pre" tags. | |
| 29 | * | |
| 30 | * @param t The string to return, enclosed in "pre" tags. | |
| 31 | * @return The value of t wrapped in "pre" tags. | |
| 32 | */ | |
| 33 | @Override | |
| 34 | public String apply( final String t ) { | |
| 35 | return STR."<pre>\{t}</pre>"; | |
| 36 | } | |
| 37 | } | |
| 1 | 38 |
| 1 | /* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * SPDX-License-Identifier: MIT | |
| 4 | */ | |
| 5 | package com.keenwrite.processors.html; | |
| 6 | ||
| 7 | import com.keenwrite.dom.DocumentParser; | |
| 8 | import com.keenwrite.io.MediaTypeExtension; | |
| 9 | import com.keenwrite.processors.ExecutorProcessor; | |
| 10 | import com.keenwrite.processors.Processor; | |
| 11 | import com.keenwrite.processors.ProcessorContext; | |
| 12 | import com.keenwrite.ui.heuristics.WordCounter; | |
| 13 | import com.keenwrite.util.DataTypeConverter; | |
| 14 | import com.whitemagicsoftware.keenquotes.parser.Contractions; | |
| 15 | import com.whitemagicsoftware.keenquotes.parser.Curler; | |
| 16 | import org.w3c.dom.Document; | |
| 17 | ||
| 18 | import java.io.File; | |
| 19 | import java.io.FileNotFoundException; | |
| 20 | import java.nio.file.Path; | |
| 21 | import java.util.*; | |
| 22 | ||
| 23 | import static com.keenwrite.Bootstrap.APP_TITLE_ABBR; | |
| 24 | import static com.keenwrite.dom.DocumentParser.*; | |
| 25 | import static com.keenwrite.events.StatusEvent.clue; | |
| 26 | import static com.keenwrite.io.SysFile.toFile; | |
| 27 | import static com.keenwrite.io.downloads.DownloadManager.open; | |
| 28 | import static com.keenwrite.util.ProtocolScheme.getProtocol; | |
| 29 | import static com.whitemagicsoftware.keenquotes.lex.FilterType.FILTER_XML; | |
| 30 | import static java.lang.String.format; | |
| 31 | import static java.lang.String.valueOf; | |
| 32 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 33 | import static java.nio.file.Files.copy; | |
| 34 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; | |
| 35 | ||
| 36 | /** | |
| 37 | * Responsible for making an XHTML document complete by wrapping it with html | |
| 38 | * and body elements. This doesn't have to be super-efficient because it's | |
| 39 | * not run in real time. | |
| 40 | */ | |
| 41 | public final class XhtmlProcessor extends ExecutorProcessor<String> { | |
| 42 | private static final Curler sTypographer = | |
| 43 | new Curler( createContractions(), FILTER_XML, true ); | |
| 44 | ||
| 45 | private final ProcessorContext mContext; | |
| 46 | ||
| 47 | public XhtmlProcessor( | |
| 48 | final Processor<String> successor, final ProcessorContext context ) { | |
| 49 | super( successor ); | |
| 50 | ||
| 51 | assert context != null; | |
| 52 | mContext = context; | |
| 53 | } | |
| 54 | ||
| 55 | /** | |
| 56 | * Responsible for producing a well-formed XML document complete with | |
| 57 | * metadata (title, author, keywords, copyright, and date). | |
| 58 | * | |
| 59 | * @param html The HTML document to transform into an XHTML document. | |
| 60 | * @return The transformed HTML document. | |
| 61 | */ | |
| 62 | @Override | |
| 63 | public String apply( final String html ) { | |
| 64 | clue( "Main.status.typeset.xhtml" ); | |
| 65 | ||
| 66 | try { | |
| 67 | final var doc = parse( html ); | |
| 68 | setMetaData( doc ); | |
| 69 | ||
| 70 | visit( doc, "//img", node -> { | |
| 71 | try { | |
| 72 | final var attrs = node.getAttributes(); | |
| 73 | final var attr = attrs.getNamedItem( "src" ); | |
| 74 | ||
| 75 | if( attr != null ) { | |
| 76 | final var src = attr.getTextContent(); | |
| 77 | final Path location; | |
| 78 | final Path imagesDir; | |
| 79 | ||
| 80 | // Download into a cache directory, which can be written to without | |
| 81 | // any possibility of overwriting local image files. Further, the | |
| 82 | // filenames are hashed as a second layer of protection. | |
| 83 | if( getProtocol( src ).isRemote() ) { | |
| 84 | location = downloadImage( src ); | |
| 85 | imagesDir = getCachesPath(); | |
| 86 | } | |
| 87 | else { | |
| 88 | location = resolveImage( src ); | |
| 89 | imagesDir = getImagesPath(); | |
| 90 | } | |
| 91 | ||
| 92 | final var relative = imagesDir.relativize( location ); | |
| 93 | ||
| 94 | attr.setTextContent( relative.toString() ); | |
| 95 | } | |
| 96 | } catch( final Exception ex ) { | |
| 97 | clue( ex ); | |
| 98 | } | |
| 99 | } ); | |
| 100 | ||
| 101 | final var document = DocumentParser.toString( doc ); | |
| 102 | final var curl = mContext.getCurlQuotes(); | |
| 103 | ||
| 104 | return curl ? sTypographer.apply( document ) : document; | |
| 105 | } catch( final Exception ex ) { | |
| 106 | clue( ex ); | |
| 107 | } | |
| 108 | ||
| 109 | return html; | |
| 110 | } | |
| 111 | ||
| 112 | /** | |
| 113 | * Applies the metadata fields to the document. | |
| 114 | * | |
| 115 | * @param doc The document to adorn with metadata. | |
| 116 | */ | |
| 117 | private void setMetaData( final Document doc ) { | |
| 118 | final var metadata = createMetaDataMap( doc ); | |
| 119 | final var title = metadata.get( "title" ); | |
| 120 | ||
| 121 | visit( doc, "/html/head", node -> { | |
| 122 | // Insert <title>text</title> inside <head>. | |
| 123 | node.appendChild( createElement( doc, "title", title ) ); | |
| 124 | // Insert <meta charset="utf-8"> inside <head>. | |
| 125 | node.appendChild( createEncoding( doc, UTF_8.toString() ) ); | |
| 126 | ||
| 127 | // Insert each <meta name=x content=y /> inside <head>. | |
| 128 | metadata.entrySet().forEach( | |
| 129 | entry -> node.appendChild( createMeta( doc, entry ) ) | |
| 130 | ); | |
| 131 | } ); | |
| 132 | } | |
| 133 | ||
| 134 | /** | |
| 135 | * Generates document metadata, including word count. | |
| 136 | * | |
| 137 | * @param doc The document containing the text to tally. | |
| 138 | * @return A map of metadata key/value pairs. | |
| 139 | */ | |
| 140 | private Map<String, String> createMetaDataMap( final Document doc ) { | |
| 141 | final var result = new LinkedHashMap<String, String>(); | |
| 142 | final var map = mContext.getInterpolatedDefinitions(); | |
| 143 | final var metadata = getMetadata(); | |
| 144 | ||
| 145 | metadata.forEach( | |
| 146 | ( key, value ) -> { | |
| 147 | final var interpolated = map.interpolate( value ); | |
| 148 | ||
| 149 | if( !interpolated.isEmpty() ) { | |
| 150 | result.put( key, interpolated ); | |
| 151 | } | |
| 152 | } | |
| 153 | ); | |
| 154 | result.put( "count", wordCount( doc ) ); | |
| 155 | ||
| 156 | return result; | |
| 157 | } | |
| 158 | ||
| 159 | /** | |
| 160 | * The metadata is in list form because the user interface for entering the | |
| 161 | * key-value pairs is a table, which requires a generic {@link List} rather | |
| 162 | * than a generic {@link Map}. | |
| 163 | * | |
| 164 | * @return The document metadata. | |
| 165 | */ | |
| 166 | private Map<String, String> getMetadata() { | |
| 167 | final var result = mContext.getMetadata(); | |
| 168 | return result == null ? new HashMap<>() : result; | |
| 169 | } | |
| 170 | ||
| 171 | /** | |
| 172 | * Hashes the URL so that the number of files doesn't eat up disk space | |
| 173 | * over time. For static resources, a feature could be added to prevent | |
| 174 | * downloading the URL if the hashed filename already exists. | |
| 175 | * | |
| 176 | * @param src The source file's URL to download. | |
| 177 | * @return A {@link Path} to the local file containing the URL's contents. | |
| 178 | * @throws Exception Could not download or save the file. | |
| 179 | */ | |
| 180 | private Path downloadImage( final String src ) throws Exception { | |
| 181 | final Path imagePath; | |
| 182 | final File imageFile; | |
| 183 | final var cachesPath = getCachesPath(); | |
| 184 | ||
| 185 | clue( "Main.status.image.xhtml.image.download", src ); | |
| 186 | ||
| 187 | try( final var response = open( src ) ) { | |
| 188 | final var mediaType = response.getMediaType(); | |
| 189 | ||
| 190 | final var ext = MediaTypeExtension.valueFrom( mediaType ).getExtension(); | |
| 191 | final var hash = DataTypeConverter.toHex( DataTypeConverter.hash( src ) ); | |
| 192 | final var id = hash.toLowerCase(); | |
| 193 | ||
| 194 | imagePath = cachesPath.resolve( APP_TITLE_ABBR + id + '.' + ext ); | |
| 195 | imageFile = toFile( imagePath ); | |
| 196 | ||
| 197 | // Preserve image files if auto-remove is turned off. | |
| 198 | if( autoRemove() ) { | |
| 199 | imageFile.deleteOnExit(); | |
| 200 | } | |
| 201 | ||
| 202 | try( final var image = response.getInputStream() ) { | |
| 203 | copy( image, imagePath, REPLACE_EXISTING ); | |
| 204 | } | |
| 205 | ||
| 206 | if( mediaType.isSvg() ) { | |
| 207 | sanitize( imagePath ); | |
| 208 | } | |
| 209 | } | |
| 210 | ||
| 211 | final var key = imageFile.exists() | |
| 212 | ? "Main.status.image.xhtml.image.saved" | |
| 213 | : "Main.status.image.xhtml.image.failed"; | |
| 214 | clue( key, imageFile ); | |
| 215 | ||
| 216 | return imagePath; | |
| 217 | } | |
| 218 | ||
| 219 | private Path resolveImage( final String src ) throws Exception { | |
| 220 | var imagePath = getImagesPath(); | |
| 221 | var found = false; | |
| 222 | ||
| 223 | Path imageFile = null; | |
| 224 | ||
| 225 | clue( "Main.status.image.xhtml.image.resolve", src ); | |
| 226 | ||
| 227 | for( final var extension : getImageOrder() ) { | |
| 228 | final var filename = format( | |
| 229 | "%s%s%s", src, extension.isBlank() ? "" : ".", extension ); | |
| 230 | imageFile = imagePath.resolve( filename ); | |
| 231 | ||
| 232 | if( toFile( imageFile ).exists() ) { | |
| 233 | found = true; | |
| 234 | break; | |
| 235 | } | |
| 236 | } | |
| 237 | ||
| 238 | if( !found ) { | |
| 239 | imagePath = getDocumentDir(); | |
| 240 | imageFile = imagePath.resolve( src ); | |
| 241 | ||
| 242 | if( !toFile( imageFile ).exists() ) { | |
| 243 | final var filename = imageFile.toString(); | |
| 244 | clue( "Main.status.image.xhtml.image.missing", filename ); | |
| 245 | ||
| 246 | throw new FileNotFoundException( filename ); | |
| 247 | } | |
| 248 | } | |
| 249 | ||
| 250 | clue( "Main.status.image.xhtml.image.found", imageFile.toString() ); | |
| 251 | ||
| 252 | return imageFile; | |
| 253 | } | |
| 254 | ||
| 255 | private Path getImagesPath() { | |
| 256 | return mContext.getImageDir(); | |
| 257 | } | |
| 258 | ||
| 259 | private Path getCachesPath() { | |
| 260 | return mContext.getCacheDir(); | |
| 261 | } | |
| 262 | ||
| 263 | /** | |
| 264 | * By including an "empty" extension, the first element returned | |
| 265 | * will be the empty string. Thus, the first extension to try is the | |
| 266 | * file's default extension. Subsequent iterations will try to find | |
| 267 | * a file that has a name matching one of the preferred extensions. | |
| 268 | * | |
| 269 | * @return A list of extensions, including an empty string at the start. | |
| 270 | */ | |
| 271 | private Iterable<String> getImageOrder() { | |
| 272 | return mContext.getImageOrder(); | |
| 273 | } | |
| 274 | ||
| 275 | /** | |
| 276 | * Returns the absolute path to the document being edited, which can be used | |
| 277 | * to find files included using relative paths. | |
| 278 | * | |
| 279 | * @return The directory containing the edited file. | |
| 280 | */ | |
| 281 | private Path getDocumentDir() { | |
| 282 | return mContext.getBaseDir(); | |
| 283 | } | |
| 284 | ||
| 285 | private Locale getLocale() { | |
| 286 | return mContext.getLocale(); | |
| 287 | } | |
| 288 | ||
| 289 | private boolean autoRemove() { | |
| 290 | return mContext.getAutoRemove(); | |
| 291 | } | |
| 292 | ||
| 293 | private String wordCount( final Document doc ) { | |
| 294 | final var sb = new StringBuilder( 65536 * 10 ); | |
| 295 | ||
| 296 | visit( | |
| 297 | doc, | |
| 298 | "//*[normalize-space( text() ) != '']", | |
| 299 | node -> sb.append( node.getTextContent() ) | |
| 300 | ); | |
| 301 | ||
| 302 | return valueOf( WordCounter.create( getLocale() ).count( sb.toString() ) ); | |
| 303 | } | |
| 304 | ||
| 305 | /** | |
| 306 | * Creates contracts with a custom set of unambiguous strings. | |
| 307 | * | |
| 308 | * @return List of contractions to use for curling straight quotes. | |
| 309 | */ | |
| 310 | private static Contractions createContractions() { | |
| 311 | return new Contractions.Builder().build(); | |
| 312 | } | |
| 313 | } | |
| 1 | 314 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * SPDX-License-Identifier: MIT | |
| 4 | */ | |
| 2 | 5 | package com.keenwrite.processors.markdown; |
| 3 | 6 |
| 9 | 9 | import com.keenwrite.processors.Processor; |
| 10 | 10 | import com.keenwrite.processors.ProcessorContext; |
| 11 | import com.keenwrite.processors.VariableProcessor; | |
| 11 | import com.keenwrite.processors.variable.VariableProcessor; | |
| 12 | 12 | import com.keenwrite.processors.markdown.extensions.caret.CaretExtension; |
| 13 | 13 | import com.keenwrite.processors.markdown.extensions.fences.FencedBlockExtension; |
| 14 | 14 | import com.keenwrite.processors.markdown.extensions.images.ImageLinkExtension; |
| 15 | 15 | import com.keenwrite.processors.markdown.extensions.outline.DocumentOutlineExtension; |
| 16 | 16 | import com.keenwrite.processors.markdown.extensions.r.RInlineExtension; |
| 17 | 17 | import com.keenwrite.processors.markdown.extensions.tex.TexExtension; |
| 18 | 18 | import com.keenwrite.processors.r.RInlineEvaluator; |
| 19 | import com.keenwrite.processors.r.RVariableProcessor; | |
| 19 | import com.keenwrite.processors.variable.RVariableProcessor; | |
| 20 | 20 | import com.vladsch.flexmark.util.misc.Extension; |
| 21 | 21 | |
| 22 | 22 | import java.util.ArrayList; |
| 23 | 23 | import java.util.List; |
| 24 | 24 | import java.util.function.Function; |
| 25 | 25 | |
| 26 | 26 | import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN; |
| 27 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | |
| 27 | import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | |
| 28 | 28 | |
| 29 | 29 | /** |
| 5 | 5 | import com.keenwrite.processors.Processor; |
| 6 | 6 | import com.keenwrite.processors.ProcessorContext; |
| 7 | import com.keenwrite.processors.VariableProcessor; | |
| 7 | import com.keenwrite.processors.variable.VariableProcessor; | |
| 8 | 8 | import com.keenwrite.processors.markdown.MarkdownProcessor; |
| 9 | 9 | import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter; |
| 10 | 10 | import com.keenwrite.processors.r.RChunkEvaluator; |
| 11 | import com.keenwrite.processors.r.RVariableProcessor; | |
| 11 | import com.keenwrite.processors.variable.RVariableProcessor; | |
| 12 | 12 | import com.vladsch.flexmark.ast.FencedCodeBlock; |
| 13 | 13 | import com.vladsch.flexmark.html.HtmlRendererOptions; |
| ... | ||
| 26 | 26 | import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; |
| 27 | 27 | import static com.keenwrite.constants.Constants.TEMPORARY_DIRECTORY; |
| 28 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | |
| 28 | import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | |
| 29 | 29 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; |
| 30 | 30 | import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT; |
| 18 | 18 | import java.util.Map; |
| 19 | 19 | |
| 20 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | |
| 20 | import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | |
| 21 | 21 | import static com.vladsch.flexmark.parser.Parser.Builder; |
| 22 | 22 |
| 32 | 32 | HTML_TEX_DELIMITED, new TexDelimitedNodeRenderer(), |
| 33 | 33 | XHTML_TEX, new TexElementNodeRenderer( true ), |
| 34 | TEXT_TEX, new TexElementNodeRenderer( true ), | |
| 34 | 35 | NONE, RENDERER |
| 35 | 36 | ); |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.pdf; | |
| 3 | ||
| 4 | import com.keenwrite.processors.ExecutorProcessor; | |
| 5 | import com.keenwrite.processors.ProcessorContext; | |
| 6 | import com.keenwrite.typesetting.Typesetter; | |
| 7 | ||
| 8 | import static com.keenwrite.Bootstrap.APP_TITLE_ABBR; | |
| 9 | import static com.keenwrite.events.StatusEvent.clue; | |
| 10 | import static com.keenwrite.io.MediaType.TEXT_XML; | |
| 11 | import static com.keenwrite.io.SysFile.normalize; | |
| 12 | import static com.keenwrite.typesetting.Typesetter.Mutator; | |
| 13 | import static com.keenwrite.util.Strings.sanitize; | |
| 14 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 15 | import static java.nio.file.Files.deleteIfExists; | |
| 16 | import static java.nio.file.Files.writeString; | |
| 17 | ||
| 18 | /** | |
| 19 | * Responsible for using a typesetting engine to convert an XHTML document | |
| 20 | * into a PDF file. This must not be run from the JavaFX thread. | |
| 21 | */ | |
| 22 | public final class PdfProcessor extends ExecutorProcessor<String> { | |
| 23 | private final ProcessorContext mProcessorContext; | |
| 24 | ||
| 25 | public PdfProcessor( final ProcessorContext context ) { | |
| 26 | assert context != null; | |
| 27 | mProcessorContext = context; | |
| 28 | } | |
| 29 | ||
| 30 | /** | |
| 31 | * Converts a document by calling a third-party application to typeset the | |
| 32 | * given XHTML document. | |
| 33 | * | |
| 34 | * @param xhtml The document to convert to a PDF file. | |
| 35 | * @return {@code null} because there is no valid return value from generating | |
| 36 | * a PDF file. | |
| 37 | */ | |
| 38 | public String apply( final String xhtml ) { | |
| 39 | try { | |
| 40 | clue( "Main.status.typeset.create" ); | |
| 41 | ||
| 42 | final var context = mProcessorContext; | |
| 43 | final var targetPath = context.getTargetPath(); | |
| 44 | clue( "Main.status.typeset.setting", "target", targetPath ); | |
| 45 | ||
| 46 | final var parent = normalize( targetPath.toAbsolutePath().getParent() ); | |
| 47 | ||
| 48 | final var document = TEXT_XML.createTempFile( APP_TITLE_ABBR, parent ); | |
| 49 | final var sourcePath = writeString( document, xhtml, UTF_8 ); | |
| 50 | clue( "Main.status.typeset.setting", "source", sourcePath ); | |
| 51 | ||
| 52 | final var themeDir = normalize( context.getThemeDir() ); | |
| 53 | clue( "Main.status.typeset.setting", "themes", themeDir ); | |
| 54 | ||
| 55 | final var imageDir = normalize( context.getImageDir() ); | |
| 56 | clue( "Main.status.typeset.setting", "images", imageDir ); | |
| 57 | ||
| 58 | final var imageOrder = context.getImageOrder(); | |
| 59 | clue( "Main.status.typeset.setting", "order", imageOrder ); | |
| 60 | ||
| 61 | final var cacheDir = normalize( context.getCacheDir() ); | |
| 62 | clue( "Main.status.typeset.setting", "caches", cacheDir ); | |
| 63 | ||
| 64 | final var fontDir = normalize( context.getFontDir() ); | |
| 65 | clue( "Main.status.typeset.setting", "fonts", fontDir ); | |
| 66 | ||
| 67 | final var rWorkDir = normalize( context.getRWorkingDir() ); | |
| 68 | clue( "Main.status.typeset.setting", "r-work", rWorkDir ); | |
| 69 | ||
| 70 | final var modesEnabled = sanitize( context.getModesEnabled() ); | |
| 71 | clue( "Main.status.typeset.setting", "mode", modesEnabled ); | |
| 72 | ||
| 73 | final var autoRemove = context.getAutoRemove(); | |
| 74 | clue( "Main.status.typeset.setting", "purge", autoRemove ); | |
| 75 | ||
| 76 | final var typesetter = Typesetter | |
| 77 | .builder() | |
| 78 | .with( Mutator::setTargetPath, targetPath ) | |
| 79 | .with( Mutator::setSourcePath, sourcePath ) | |
| 80 | .with( Mutator::setThemeDir, themeDir ) | |
| 81 | .with( Mutator::setImageDir, imageDir ) | |
| 82 | .with( Mutator::setCacheDir, cacheDir ) | |
| 83 | .with( Mutator::setFontDir, fontDir ) | |
| 84 | .with( Mutator::setModesEnabled, modesEnabled ) | |
| 85 | .with( Mutator::setAutoRemove, autoRemove ) | |
| 86 | .build(); | |
| 87 | ||
| 88 | try { | |
| 89 | typesetter.typeset(); | |
| 90 | } | |
| 91 | finally { | |
| 92 | // Smote the temporary file after typesetting the document. | |
| 93 | if( typesetter.autoRemove() ) { | |
| 94 | deleteIfExists( document ); | |
| 95 | } | |
| 96 | } | |
| 97 | } catch( final Exception ex ) { | |
| 98 | // Typesetter runtime exceptions will pass up the call stack. | |
| 99 | clue( "Main.status.typeset.failed", ex ); | |
| 100 | } | |
| 101 | ||
| 102 | // Do not continue processing (the document was typeset into a binary). | |
| 103 | return null; | |
| 104 | } | |
| 105 | } | |
| 1 | 106 |
| 15 | 15 | import static com.keenwrite.preferences.AppKeys.KEY_R_DIR; |
| 16 | 16 | import static com.keenwrite.preferences.AppKeys.KEY_R_SCRIPT; |
| 17 | import static com.keenwrite.processors.r.RVariableProcessor.escape; | |
| 17 | import static com.keenwrite.processors.variable.RVariableProcessor.escape; | |
| 18 | 18 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; |
| 19 | 19 |
| 1 | /* Copyright 2023 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * SPDX-License-Identifier: MIT | |
| 4 | */ | |
| 5 | package com.keenwrite.processors.r; | |
| 6 | ||
| 7 | import com.keenwrite.processors.ExecutorProcessor; | |
| 8 | import com.keenwrite.processors.Processor; | |
| 9 | import com.keenwrite.processors.ProcessorContext; | |
| 10 | ||
| 11 | public class RBootstrapProcessor extends ExecutorProcessor<String> { | |
| 12 | private final Processor<String> mSuccessor; | |
| 13 | private final ProcessorContext mContext; | |
| 14 | ||
| 15 | public RBootstrapProcessor( | |
| 16 | final Processor<String> successor, | |
| 17 | final ProcessorContext context ) { | |
| 18 | assert successor != null; | |
| 19 | assert context != null; | |
| 20 | ||
| 21 | mSuccessor = successor; | |
| 22 | mContext = context; | |
| 23 | } | |
| 24 | ||
| 25 | /** | |
| 26 | * Processes the given text document by replacing variables with their values. | |
| 27 | * | |
| 28 | * @param text The document text that includes variables that should be | |
| 29 | * replaced with values when rendered as HTML. | |
| 30 | * @return The text with all variables replaced. | |
| 31 | */ | |
| 32 | @Override | |
| 33 | public String apply( final String text ) { | |
| 34 | assert text != null; | |
| 35 | ||
| 36 | final var bootstrap = mContext.getRScript(); | |
| 37 | final var workingDir = mContext.getRWorkingDir().toString(); | |
| 38 | final var definitions = mContext.getDefinitions(); | |
| 39 | ||
| 40 | RBootstrapController.update( bootstrap, workingDir, definitions ); | |
| 41 | ||
| 42 | return mSuccessor.apply( text ); | |
| 43 | } | |
| 44 | } | |
| 1 | 45 |
| 3 | 3 | |
| 4 | 4 | import com.keenwrite.processors.Processor; |
| 5 | import com.keenwrite.processors.variable.RVariableProcessor; | |
| 5 | 6 | |
| 6 | 7 | import java.util.function.Function; |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.r; | |
| 3 | ||
| 4 | import com.keenwrite.processors.Processor; | |
| 5 | import com.keenwrite.processors.ProcessorContext; | |
| 6 | import com.keenwrite.processors.VariableProcessor; | |
| 7 | import com.keenwrite.sigils.RKeyOperator; | |
| 8 | import com.keenwrite.sigils.SigilKeyOperator; | |
| 9 | ||
| 10 | import java.util.function.UnaryOperator; | |
| 11 | ||
| 12 | /** | |
| 13 | * Converts the keys of the resolved map from default form to R form, then | |
| 14 | * performs a substitution on the text. The default R variable syntax is | |
| 15 | * <pre>v$tree$leaf</pre>. | |
| 16 | */ | |
| 17 | public class RVariableProcessor extends VariableProcessor { | |
| 18 | public RVariableProcessor( | |
| 19 | final Processor<String> successor, final ProcessorContext context ) { | |
| 20 | super( successor, context ); | |
| 21 | } | |
| 22 | ||
| 23 | @Override | |
| 24 | protected SigilKeyOperator createKeyOperator( | |
| 25 | final ProcessorContext context ) { | |
| 26 | return new RKeyOperator(); | |
| 27 | } | |
| 28 | ||
| 29 | @Override | |
| 30 | protected String processValue( final String value ) { | |
| 31 | assert value != null; | |
| 32 | ||
| 33 | return escape( value ); | |
| 34 | } | |
| 35 | ||
| 36 | /** | |
| 37 | * In R, single quotes and double quotes are interchangeable. Using single | |
| 38 | * quotes is simpler to code. | |
| 39 | * | |
| 40 | * @param value The text to convert into a valid quoted R string. | |
| 41 | * @return The quoted value with embedded quotes escaped as necessary. | |
| 42 | */ | |
| 43 | public static String escape( final String value ) { | |
| 44 | return '\'' + escape( value, '\'', "\\'" ) + '\''; | |
| 45 | } | |
| 46 | ||
| 47 | /** | |
| 48 | * TODO: Make generic method for replacing text. | |
| 49 | * | |
| 50 | * @param haystack Search this string for the needle, must not be null. | |
| 51 | * @param needle The character to find in the haystack. | |
| 52 | * @param thread Replace the needle with this text, if the needle is found. | |
| 53 | * @return The haystack with the all instances of needle replaced with thread. | |
| 54 | */ | |
| 55 | @SuppressWarnings( "SameParameterValue" ) | |
| 56 | private static String escape( | |
| 57 | final String haystack, final char needle, final String thread ) { | |
| 58 | assert haystack != null; | |
| 59 | assert thread != null; | |
| 60 | ||
| 61 | int end = haystack.indexOf( needle ); | |
| 62 | ||
| 63 | if( end < 0 ) { | |
| 64 | return haystack; | |
| 65 | } | |
| 66 | ||
| 67 | int start = 0; | |
| 68 | ||
| 69 | // Replace up to 32 occurrences before reallocating the internal buffer. | |
| 70 | final var sb = new StringBuilder( haystack.length() + 32 ); | |
| 71 | ||
| 72 | while( end >= 0 ) { | |
| 73 | sb.append( haystack, start, end ).append( thread ); | |
| 74 | start = end + 1; | |
| 75 | end = haystack.indexOf( needle, start ); | |
| 76 | } | |
| 77 | ||
| 78 | return sb.append( haystack.substring( start ) ).toString(); | |
| 79 | } | |
| 80 | } | |
| 81 | 1 |
| 1 | /* Copyright 2024 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * SPDX-License-Identifier: MIT | |
| 4 | */ | |
| 5 | package com.keenwrite.processors.text; | |
| 6 | ||
| 7 | import com.keenwrite.io.MediaType; | |
| 8 | import com.keenwrite.processors.ExecutorProcessor; | |
| 9 | import com.keenwrite.processors.Processor; | |
| 10 | import com.keenwrite.processors.ProcessorContext; | |
| 11 | import com.keenwrite.processors.r.RInlineEvaluator; | |
| 12 | import com.keenwrite.processors.variable.RVariableProcessor; | |
| 13 | import com.keenwrite.processors.variable.VariableProcessor; | |
| 14 | ||
| 15 | import java.util.function.Function; | |
| 16 | ||
| 17 | import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN; | |
| 18 | import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY; | |
| 19 | ||
| 20 | /** | |
| 21 | * Responsible for converting documents to plain text files. This will | |
| 22 | * perform interpolated variable substitutions and execute R commands | |
| 23 | * as necessary. | |
| 24 | */ | |
| 25 | public class TextProcessor extends ExecutorProcessor<String> { | |
| 26 | private final Function<String, String> mEvaluator; | |
| 27 | ||
| 28 | public TextProcessor( | |
| 29 | final Processor<String> successor, | |
| 30 | final ProcessorContext context ) { | |
| 31 | super( successor ); | |
| 32 | ||
| 33 | final var inputPath = context.getSourcePath(); | |
| 34 | final var mediaType = MediaType.fromFilename( inputPath ); | |
| 35 | ||
| 36 | if( mediaType == TEXT_R_MARKDOWN ) { | |
| 37 | final var rVarProcessor = new RVariableProcessor( IDENTITY, context ); | |
| 38 | mEvaluator = new RInlineEvaluator( rVarProcessor ); | |
| 39 | } | |
| 40 | else { | |
| 41 | mEvaluator = new VariableProcessor( IDENTITY, context ); | |
| 42 | } | |
| 43 | } | |
| 44 | ||
| 45 | @Override | |
| 46 | public String apply( final String document ) { | |
| 47 | return mEvaluator.apply( document ); | |
| 48 | } | |
| 49 | } | |
| 1 | 50 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * SPDX-License-Identifier: MIT | |
| 4 | */ | |
| 5 | package com.keenwrite.processors.variable; | |
| 6 | ||
| 7 | import com.keenwrite.processors.Processor; | |
| 8 | import com.keenwrite.processors.ProcessorContext; | |
| 9 | import com.keenwrite.sigils.RKeyOperator; | |
| 10 | import com.keenwrite.sigils.SigilKeyOperator; | |
| 11 | ||
| 12 | /** | |
| 13 | * Converts the keys of the resolved map from default form to R form, then | |
| 14 | * performs a substitution on the text. The default R variable syntax is | |
| 15 | * <pre>v$tree$leaf</pre>. | |
| 16 | */ | |
| 17 | public class RVariableProcessor extends VariableProcessor { | |
| 18 | public RVariableProcessor( | |
| 19 | final Processor<String> successor, final ProcessorContext context ) { | |
| 20 | super( successor, context ); | |
| 21 | } | |
| 22 | ||
| 23 | @Override | |
| 24 | protected SigilKeyOperator createKeyOperator( | |
| 25 | final ProcessorContext context ) { | |
| 26 | return new RKeyOperator(); | |
| 27 | } | |
| 28 | ||
| 29 | @Override | |
| 30 | protected String processValue( final String value ) { | |
| 31 | assert value != null; | |
| 32 | ||
| 33 | return escape( value ); | |
| 34 | } | |
| 35 | ||
| 36 | /** | |
| 37 | * In R, single quotes and double quotes are interchangeable. Using single | |
| 38 | * quotes is simpler to code. | |
| 39 | * | |
| 40 | * @param value The text to convert into a valid quoted R string. | |
| 41 | * @return The quoted value with embedded quotes escaped as necessary. | |
| 42 | */ | |
| 43 | public static String escape( final String value ) { | |
| 44 | return '\'' + escape( value, '\'', "\\'" ) + '\''; | |
| 45 | } | |
| 46 | ||
| 47 | /** | |
| 48 | * TODO: Make generic method for replacing text. | |
| 49 | * | |
| 50 | * @param haystack Search this string for the needle, must not be null. | |
| 51 | * @param needle The character to find in the haystack. | |
| 52 | * @param thread Replace the needle with this text, if the needle is found. | |
| 53 | * @return The haystack with the all instances of needle replaced with thread. | |
| 54 | */ | |
| 55 | @SuppressWarnings( "SameParameterValue" ) | |
| 56 | private static String escape( | |
| 57 | final String haystack, final char needle, final String thread ) { | |
| 58 | assert haystack != null; | |
| 59 | assert thread != null; | |
| 60 | ||
| 61 | int end = haystack.indexOf( needle ); | |
| 62 | ||
| 63 | if( end < 0 ) { | |
| 64 | return haystack; | |
| 65 | } | |
| 66 | ||
| 67 | int start = 0; | |
| 68 | ||
| 69 | // Replace up to 32 occurrences before reallocating the internal buffer. | |
| 70 | final var sb = new StringBuilder( haystack.length() + 32 ); | |
| 71 | ||
| 72 | while( end >= 0 ) { | |
| 73 | sb.append( haystack, start, end ).append( thread ); | |
| 74 | start = end + 1; | |
| 75 | end = haystack.indexOf( needle, start ); | |
| 76 | } | |
| 77 | ||
| 78 | return sb.append( haystack.substring( start ) ).toString(); | |
| 79 | } | |
| 80 | } | |
| 1 | 81 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.variable; | |
| 3 | ||
| 4 | import com.keenwrite.processors.ExecutorProcessor; | |
| 5 | import com.keenwrite.processors.Processor; | |
| 6 | import com.keenwrite.processors.ProcessorContext; | |
| 7 | import com.keenwrite.sigils.SigilKeyOperator; | |
| 8 | ||
| 9 | import java.util.HashMap; | |
| 10 | import java.util.Map; | |
| 11 | import java.util.function.Function; | |
| 12 | ||
| 13 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; | |
| 14 | ||
| 15 | /** | |
| 16 | * Processes interpolated string definitions in the document and inserts | |
| 17 | * their values into the post-processed text. The default variable syntax is | |
| 18 | * <pre>{{variable}}</pre> (a.k.a., moustache syntax). | |
| 19 | */ | |
| 20 | public class VariableProcessor | |
| 21 | extends ExecutorProcessor<String> implements Function<String, String> { | |
| 22 | ||
| 23 | private final ProcessorContext mContext; | |
| 24 | private final SigilKeyOperator mSigilOperator; | |
| 25 | ||
| 26 | /** | |
| 27 | * Constructs a processor capable of interpolating string definitions. | |
| 28 | * | |
| 29 | * @param successor Subsequent link in the processing chain. | |
| 30 | * @param context Contains resolved definitions map. | |
| 31 | */ | |
| 32 | public VariableProcessor( | |
| 33 | final Processor<String> successor, | |
| 34 | final ProcessorContext context ) { | |
| 35 | super( successor ); | |
| 36 | ||
| 37 | mContext = context; | |
| 38 | mSigilOperator = createKeyOperator( context ); | |
| 39 | } | |
| 40 | ||
| 41 | /** | |
| 42 | * Subclasses may change the type of operation performed on keys, such as | |
| 43 | * wrapping key names in sigils. | |
| 44 | * | |
| 45 | * @param context Provides the name of the file being edited. | |
| 46 | * @return An operator for transforming key names. | |
| 47 | */ | |
| 48 | protected SigilKeyOperator createKeyOperator( | |
| 49 | final ProcessorContext context ) { | |
| 50 | return context.createKeyOperator(); | |
| 51 | } | |
| 52 | ||
| 53 | /** | |
| 54 | * Returns the map to use for variable substitution. | |
| 55 | * | |
| 56 | * @return A map of variable names to values, with keys wrapped in sigils. | |
| 57 | */ | |
| 58 | public Map<String, String> getDefinitions() { | |
| 59 | return entoken( mContext.getInterpolatedDefinitions() ); | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * Subclasses may override this method to change how keys are wrapped | |
| 64 | * in sigils. | |
| 65 | * | |
| 66 | * @param key The key to enwrap. | |
| 67 | * @return The wrapped key. | |
| 68 | */ | |
| 69 | protected String processKey( final String key ) { | |
| 70 | return mSigilOperator.apply( key ); | |
| 71 | } | |
| 72 | ||
| 73 | /** | |
| 74 | * Subclasses may override this method to modify values prior to use. This | |
| 75 | * can be used, for example, to escape values prior to evaluating by a | |
| 76 | * scripting engine. | |
| 77 | * | |
| 78 | * @param value The value to process. | |
| 79 | * @return The processed value. | |
| 80 | */ | |
| 81 | protected String processValue( final String value ) { | |
| 82 | return value; | |
| 83 | } | |
| 84 | ||
| 85 | /** | |
| 86 | * Answers whether the given key is wrapped in sigil tokens. | |
| 87 | * | |
| 88 | * @param key The key to analyze. | |
| 89 | * @return {@code true} if the key is wrapped in sigils. | |
| 90 | */ | |
| 91 | public boolean hasSigils( final String key ) { | |
| 92 | return mSigilOperator.match( key ).find(); | |
| 93 | } | |
| 94 | ||
| 95 | /** | |
| 96 | * Processes the given text document by replacing variables with their values. | |
| 97 | * | |
| 98 | * @param text The document text that includes variables that should be | |
| 99 | * replaced with values when rendered as HTML. | |
| 100 | * @return The text with all variables replaced. | |
| 101 | */ | |
| 102 | @Override | |
| 103 | public String apply( final String text ) { | |
| 104 | assert text != null; | |
| 105 | ||
| 106 | return replace( text, getDefinitions() ); | |
| 107 | } | |
| 108 | ||
| 109 | /** | |
| 110 | * Converts the given map from regular variables to processor-specific | |
| 111 | * variables. | |
| 112 | * | |
| 113 | * @param map Map of variable names to values. | |
| 114 | * @return Map of variables with the keys and values subjected to | |
| 115 | * post-processing. | |
| 116 | */ | |
| 117 | protected Map<String, String> entoken( final Map<String, String> map ) { | |
| 118 | assert map != null; | |
| 119 | ||
| 120 | final var result = new HashMap<String, String>( map.size() ); | |
| 121 | ||
| 122 | map.forEach( ( k, v ) -> result.put( processKey( k ), processValue( v ) ) ); | |
| 123 | ||
| 124 | return result; | |
| 125 | } | |
| 126 | } | |
| 1 | 127 |
| 79 | 79 | addAction( "file.export.pdf.repeat", _ -> actions.file_export_repeat() ), |
| 80 | 80 | addAction( "file.export.html.dir", _ -> actions.file_export_html_dir() ), |
| 81 | addAction( "file.export.text_tex.dir", _ -> actions.file_export_text_tex_dir() ), | |
| 81 | 82 | addAction( "file.export.html_svg", _ -> actions.file_export_html_svg() ), |
| 82 | 83 | addAction( "file.export.html_tex", _ -> actions.file_export_html_tex() ), |
| 84 | addAction( "file.export.text_tex", _ -> actions.file_export_text_tex() ), | |
| 83 | 85 | addAction( "file.export.xhtml_tex", _ -> actions.file_export_xhtml_tex() ) |
| 84 | 86 | ), |
| 44 | 44 | import static com.keenwrite.ExportFormat.*; |
| 45 | 45 | import static com.keenwrite.Messages.get; |
| 46 | import static com.keenwrite.constants.Constants.PDF_DEFAULT; | |
| 47 | 46 | import static com.keenwrite.constants.Constants.USER_DIRECTORY; |
| 48 | 47 | import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE; |
| ... | ||
| 196 | 195 | : userHomeParent; |
| 197 | 196 | |
| 198 | final var filename = format.toExportFilename( editor.getPath() ); | |
| 199 | final var selected = PDF_DEFAULT | |
| 200 | .getName() | |
| 201 | .equals( exported.get().getName() ); | |
| 197 | final var filename = format.toExportFilename( exported.get() ); | |
| 198 | ||
| 202 | 199 | final var selection = pickFile( |
| 203 | selected | |
| 204 | ? filename | |
| 205 | : exported.get(), | |
| 200 | filename, | |
| 206 | 201 | exportPath, |
| 207 | 202 | FILE_EXPORT |
| ... | ||
| 337 | 332 | public void file_export_html_tex() { |
| 338 | 333 | file_export( HTML_TEX_DELIMITED ); |
| 334 | } | |
| 335 | ||
| 336 | public void file_export_text_tex() { | |
| 337 | file_export( TEXT_TEX, false ); | |
| 338 | } | |
| 339 | ||
| 340 | public void file_export_text_tex_dir() { | |
| 341 | file_export( TEXT_TEX, true ); | |
| 339 | 342 | } |
| 340 | 343 | |
| 94 | 94 | @Override |
| 95 | 95 | public void setInitialDirectory( final Path path ) { |
| 96 | final var file = toFile( path ); | |
| 96 | final var directory = toFile( path ); | |
| 97 | 97 | |
| 98 | 98 | mChooser.setInitialDirectory( |
| 99 | file.exists() ? file : new File( getUserHome() ) | |
| 99 | directory.exists() ? directory : new File( getUserHome() ) | |
| 100 | 100 | ); |
| 101 | 101 | } |
| 570 | 570 | Action.file.export.pdf.dir.icon=FILE_PDF_ALT |
| 571 | 571 | |
| 572 | Action.file.export.text_tex.dir.description=Convert files in document directory | |
| 573 | Action.file.export.text_tex.dir.text=Joined Text | |
| 574 | Action.file.export.text_tex.dir.icon=FILE_TEXT_ALT | |
| 575 | ||
| 572 | 576 | Action.file.export.pdf.repeat.description=Repeat previous typesetting command |
| 573 | 577 | Action.file.export.pdf.repeat.accelerator=Ctrl+Shift+E |
| ... | ||
| 586 | 590 | Action.file.export.html_tex.description=Export the current document as HTML + TeX |
| 587 | 591 | Action.file.export.html_tex.text=HTML and _TeX |
| 592 | ||
| 593 | Action.file.export.text_tex.description=Export the current document as text + TeX | |
| 594 | Action.file.export.text_tex.text=Text and TeX | |
| 588 | 595 | |
| 589 | 596 | Action.file.export.xhtml_tex.description=Export as XHTML + TeX |