Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M BUILD.md
1818
Clone the repository as follows:
1919
20
    git clone https://gitlab.com/DaveJarvis/KeenWrite.git
20
    git clone https://gitlab.com/DaveJarvis/KeenWrite.git keenwrite
2121
2222
The repository is cloned.
M bug-filter.xml
1212
  </Match>
1313
14
  <Match class="com.keenwrite.processors.HtmlPreviewProcessor">
14
  <Match class="com.keenwrite.processors.html.HtmlPreviewProcessor">
1515
    <Method name="&lt;init&gt;" />
1616
    <Bug code="ST" />
M src/main/java/com/keenwrite/AppCommands.java
1010
import com.keenwrite.processors.Processor;
1111
import com.keenwrite.processors.ProcessorContext;
12
import com.keenwrite.processors.RBootstrapProcessor;
12
import com.keenwrite.processors.r.RBootstrapProcessor;
1313
1414
import java.io.IOException;
M src/main/java/com/keenwrite/ExportFormat.java
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
 */
25
package com.keenwrite;
36
...
3235
   * For XHTML exports, encode TeX using {@code $} delimiters.
3336
   */
34
  XHTML_TEX( ".xml" ),
37
  XHTML_TEX( ".xhtml" ),
38
39
  /**
40
   * For TEXT exports, encode TeX using {@code $} delimiters.
41
   */
42
  TEXT_TEX( ".txt" ),
3543
3644
  /**
M src/main/java/com/keenwrite/MainPane.java
1818
import com.keenwrite.preferences.Workspace;
1919
import com.keenwrite.preview.HtmlPreview;
20
import com.keenwrite.processors.HtmlPreviewProcessor;
20
import com.keenwrite.processors.html.HtmlPreviewProcessor;
2121
import com.keenwrite.processors.Processor;
2222
import com.keenwrite.processors.ProcessorContext;
...
7777
import static com.keenwrite.io.SysFile.toFile;
7878
import static com.keenwrite.preferences.AppKeys.*;
79
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
79
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
8080
import static com.keenwrite.processors.ProcessorContext.Mutator;
8181
import static com.keenwrite.processors.ProcessorContext.builder;
D src/main/java/com/keenwrite/processors/HtmlPreviewProcessor.java
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
}
431
D src/main/java/com/keenwrite/processors/IdentityProcessor.java
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
}
291
D src/main/java/com/keenwrite/processors/PdfProcessor.java
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
}
1041
D src/main/java/com/keenwrite/processors/PreformattedProcessor.java
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
}
321
M src/main/java/com/keenwrite/processors/ProcessorContext.java
1212
import com.keenwrite.io.MediaType;
1313
import com.keenwrite.io.MediaTypeExtension;
14
import com.keenwrite.processors.variable.VariableProcessor;
1415
import com.keenwrite.sigils.PropertyKeyOperator;
1516
import com.keenwrite.sigils.SigilKeyOperator;
...
3233
import static com.keenwrite.io.SysFile.toFile;
3334
import static com.keenwrite.predicates.PredicateFactory.createFileTypePredicate;
34
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
35
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
3536
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
3637
import static com.keenwrite.util.Strings.sanitize;
M src/main/java/com/keenwrite/processors/ProcessorFactory.java
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
 */
25
package com.keenwrite.processors;
36
7
import com.keenwrite.processors.html.PreformattedProcessor;
8
import com.keenwrite.processors.html.XhtmlProcessor;
49
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;
513
14
import static com.keenwrite.ExportFormat.TEXT_TEX;
615
import static com.keenwrite.io.FileType.RMARKDOWN;
716
import static com.keenwrite.io.FileType.SOURCE;
8
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
17
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
918
1019
/**
...
5867
      case NONE -> preview;
5968
      case XHTML_TEX -> createXhtmlProcessor( context );
69
      case TEXT_TEX -> createTextProcessor( context );
6070
      case APPLICATION_PDF -> createPdfProcessor( context );
6171
      default -> createIdentityProcessor( context );
6272
    };
6373
6474
    final var inputType = context.getSourceFileType();
6575
    final Processor<String> processor;
6676
67
    // When there's no preview, convert to HTML.
6877
    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
      }
7084
    }
7185
    else {
...
121135
    final ProcessorContext context ) {
122136
    return createXhtmlProcessor( IDENTITY, context );
137
  }
138
139
  private static Processor<String> createTextProcessor(
140
    final ProcessorContext context ) {
141
    return new TextProcessor( IDENTITY, context );
123142
  }
124143
D src/main/java/com/keenwrite/processors/RBootstrapProcessor.java
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
}
431
D src/main/java/com/keenwrite/processors/VariableProcessor.java
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
}
1241
D src/main/java/com/keenwrite/processors/XhtmlProcessor.java
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
}
3111
A src/main/java/com/keenwrite/processors/html/HtmlPreviewProcessor.java
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
}
144
A src/main/java/com/keenwrite/processors/html/IdentityProcessor.java
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
}
131
A src/main/java/com/keenwrite/processors/html/PreformattedProcessor.java
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
}
138
A src/main/java/com/keenwrite/processors/html/XhtmlProcessor.java
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
}
1314
M src/main/java/com/keenwrite/processors/markdown/BaseMarkdownProcessor.java
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
 */
25
package com.keenwrite.processors.markdown;
36
M src/main/java/com/keenwrite/processors/markdown/MarkdownProcessor.java
99
import com.keenwrite.processors.Processor;
1010
import com.keenwrite.processors.ProcessorContext;
11
import com.keenwrite.processors.VariableProcessor;
11
import com.keenwrite.processors.variable.VariableProcessor;
1212
import com.keenwrite.processors.markdown.extensions.caret.CaretExtension;
1313
import com.keenwrite.processors.markdown.extensions.fences.FencedBlockExtension;
1414
import com.keenwrite.processors.markdown.extensions.images.ImageLinkExtension;
1515
import com.keenwrite.processors.markdown.extensions.outline.DocumentOutlineExtension;
1616
import com.keenwrite.processors.markdown.extensions.r.RInlineExtension;
1717
import com.keenwrite.processors.markdown.extensions.tex.TexExtension;
1818
import com.keenwrite.processors.r.RInlineEvaluator;
19
import com.keenwrite.processors.r.RVariableProcessor;
19
import com.keenwrite.processors.variable.RVariableProcessor;
2020
import com.vladsch.flexmark.util.misc.Extension;
2121
2222
import java.util.ArrayList;
2323
import java.util.List;
2424
import java.util.function.Function;
2525
2626
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;
2828
2929
/**
M src/main/java/com/keenwrite/processors/markdown/extensions/fences/FencedBlockExtension.java
55
import com.keenwrite.processors.Processor;
66
import com.keenwrite.processors.ProcessorContext;
7
import com.keenwrite.processors.VariableProcessor;
7
import com.keenwrite.processors.variable.VariableProcessor;
88
import com.keenwrite.processors.markdown.MarkdownProcessor;
99
import com.keenwrite.processors.markdown.extensions.common.HtmlRendererAdapter;
1010
import com.keenwrite.processors.r.RChunkEvaluator;
11
import com.keenwrite.processors.r.RVariableProcessor;
11
import com.keenwrite.processors.variable.RVariableProcessor;
1212
import com.vladsch.flexmark.ast.FencedCodeBlock;
1313
import com.vladsch.flexmark.html.HtmlRendererOptions;
...
2626
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
2727
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;
2929
import static com.vladsch.flexmark.html.HtmlRenderer.Builder;
3030
import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT;
M src/main/java/com/keenwrite/processors/markdown/extensions/r/RInlineExtension.java
1818
import java.util.Map;
1919
20
import static com.keenwrite.processors.IdentityProcessor.IDENTITY;
20
import static com.keenwrite.processors.html.IdentityProcessor.IDENTITY;
2121
import static com.vladsch.flexmark.parser.Parser.Builder;
2222
M src/main/java/com/keenwrite/processors/markdown/extensions/tex/TexNodeRenderer.java
3232
      HTML_TEX_DELIMITED, new TexDelimitedNodeRenderer(),
3333
      XHTML_TEX, new TexElementNodeRenderer( true ),
34
      TEXT_TEX, new TexElementNodeRenderer( true ),
3435
      NONE, RENDERER
3536
    );
A src/main/java/com/keenwrite/processors/pdf/PdfProcessor.java
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
}
1106
M src/main/java/com/keenwrite/processors/r/RBootstrapController.java
1515
import static com.keenwrite.preferences.AppKeys.KEY_R_DIR;
1616
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;
1818
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
1919
A src/main/java/com/keenwrite/processors/r/RBootstrapProcessor.java
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
}
145
M src/main/java/com/keenwrite/processors/r/RInlineEvaluator.java
33
44
import com.keenwrite.processors.Processor;
5
import com.keenwrite.processors.variable.RVariableProcessor;
56
67
import java.util.function.Function;
D src/main/java/com/keenwrite/processors/r/RVariableProcessor.java
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
}
811
A src/main/java/com/keenwrite/processors/text/TextProcessor.java
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
}
150
A src/main/java/com/keenwrite/processors/variable/RVariableProcessor.java
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
}
181
A src/main/java/com/keenwrite/processors/variable/VariableProcessor.java
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
}
1127
M src/main/java/com/keenwrite/ui/actions/ApplicationBars.java
7979
          addAction( "file.export.pdf.repeat", _ -> actions.file_export_repeat() ),
8080
          addAction( "file.export.html.dir", _ -> actions.file_export_html_dir() ),
81
          addAction( "file.export.text_tex.dir", _ -> actions.file_export_text_tex_dir() ),
8182
          addAction( "file.export.html_svg", _ -> actions.file_export_html_svg() ),
8283
          addAction( "file.export.html_tex", _ -> actions.file_export_html_tex() ),
84
          addAction( "file.export.text_tex", _ -> actions.file_export_text_tex() ),
8385
          addAction( "file.export.xhtml_tex", _ -> actions.file_export_xhtml_tex() )
8486
        ),
M src/main/java/com/keenwrite/ui/actions/GuiCommands.java
4444
import static com.keenwrite.ExportFormat.*;
4545
import static com.keenwrite.Messages.get;
46
import static com.keenwrite.constants.Constants.PDF_DEFAULT;
4746
import static com.keenwrite.constants.Constants.USER_DIRECTORY;
4847
import static com.keenwrite.constants.GraphicsConstants.ICON_DIALOG_NODE;
...
196195
      : userHomeParent;
197196
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
202199
    final var selection = pickFile(
203
      selected
204
        ? filename
205
        : exported.get(),
200
      filename,
206201
      exportPath,
207202
      FILE_EXPORT
...
337332
  public void file_export_html_tex() {
338333
    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 );
339342
  }
340343
M src/main/java/com/keenwrite/ui/explorer/FilePickerFactory.java
9494
    @Override
9595
    public void setInitialDirectory( final Path path ) {
96
      final var file = toFile( path );
96
      final var directory = toFile( path );
9797
9898
      mChooser.setInitialDirectory(
99
        file.exists() ? file : new File( getUserHome() )
99
        directory.exists() ? directory : new File( getUserHome() )
100100
      );
101101
    }
M src/main/resources/com/keenwrite/messages.properties
570570
Action.file.export.pdf.dir.icon=FILE_PDF_ALT
571571
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
572576
Action.file.export.pdf.repeat.description=Repeat previous typesetting command
573577
Action.file.export.pdf.repeat.accelerator=Ctrl+Shift+E
...
586590
Action.file.export.html_tex.description=Export the current document as HTML + TeX
587591
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
588595
589596
Action.file.export.xhtml_tex.description=Export as XHTML + TeX