Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
M README.md
33
# ![Logo](docs/images/app-title.png)
44
5
A text editor that uses [interpolated strings](https://en.wikipedia.org/wiki/String_interpolation) to reference externally defined values.
5
A text editor that uses [interpolated strings](https://en.wikipedia.org/wiki/String_interpolation) to reference values defined externally.
66
77
## Download
M build.gradle
5050
5151
dependencies {
52
  def v_junit = '5.7.1'
52
  def v_junit = '5.7.2'
5353
  def v_flexmark = '0.62.2'
54
  def v_jackson = '2.12.2'
54
  def v_jackson = '2.12.3'
5555
  def v_batik = '1.14'
5656
  def v_wheatsheaf = '2.0.1'
5757
5858
  // JavaFX
5959
  implementation 'org.controlsfx:controlsfx:11.1.0'
6060
  implementation 'org.fxmisc.richtext:richtextfx:0.10.6'
6161
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
62
  implementation 'com.miglayout:miglayout-javafx:5.2'
62
  implementation 'com.miglayout:miglayout-javafx:11.0'
6363
  implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.8.0'
6464
...
8181
  implementation "com.fasterxml.jackson.core:jackson-annotations:${v_jackson}"
8282
  implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${v_jackson}"
83
  implementation 'org.yaml:snakeyaml:1.27'
83
  implementation 'org.yaml:snakeyaml:1.29'
8484
8585
  // XML
...
117117
  implementation 'org.greenrobot:eventbus:3.2.0'
118118
119
  // Configuration: Update Workspace to use Jackson, instead could shave ~800kb
119
  // TODO: Update Workspace config to use Jackson to shave ~800kb
120120
  implementation 'org.apache.commons:commons-configuration2:2.7'
121121
  implementation 'commons-beanutils:commons-beanutils:1.9.4'
122122
123
  // Spelling, TeX, Docking
123
  // Spelling, TeX, Docking, KeenQuotes
124124
  implementation fileTree(include: ['**/*.jar'], dir: 'libs')
125125
M docs/typesetting.md
8080
Install and configure the default theme pack as follows:
8181
82
1. Download the <a href="https://github.com/DaveJarvis/keenwrite-themes/raw/main/theme-pack.zip">theme-pack.zip</a> archive.
82
1. Download the <a href="https://gitreleases.dev/gh/DaveJarvis/keenwrite-themes/latest/theme-pack.zip">theme-pack.zip</a> archive.
8383
1. Extract archive into a known location.
8484
1. Start the text editor, if not already running.
A libs/keenquotes.jar
Binary file
A src/main/java/com/keenwrite/dom/DocumentConverter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.dom;
3
4
import org.jsoup.helper.W3CDom;
5
import org.jsoup.nodes.Node;
6
import org.jsoup.nodes.TextNode;
7
import org.jsoup.select.NodeVisitor;
8
import org.w3c.dom.Document;
9
10
import java.util.LinkedHashMap;
11
import java.util.Map;
12
13
import static com.keenwrite.dom.DocumentParser.sDomImplementation;
14
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
15
16
/**
17
 * Responsible for converting JSoup document object model (DOM) to a W3C DOM.
18
 * Provides a lighter implementation than the superclass by overriding the
19
 * {@link #fromJsoup(org.jsoup.nodes.Document)} method to reuse factories,
20
 * builders, and implementations.
21
 */
22
public final class DocumentConverter extends W3CDom {
23
  /**
24
   * Retain insertion order using an instance of {@link LinkedHashMap} so
25
   * that ligature substitution uses longer ligatures ahead of shorter
26
   * ligatures. The word "ruffian" should use the "ffi" ligature, not the "ff"
27
   * ligature.
28
   */
29
  private static final Map<String, String> LIGATURES = new LinkedHashMap<>();
30
31
  static {
32
    LIGATURES.put( "ffi", "\uFB03" );
33
    LIGATURES.put( "ffl", "\uFB04" );
34
    LIGATURES.put( "ff", "\uFB00" );
35
    LIGATURES.put( "fi", "\uFB01" );
36
    LIGATURES.put( "fl", "\uFB02" );
37
  }
38
39
  private static final NodeVisitor LIGATURE_VISITOR = new NodeVisitor() {
40
    @Override
41
    public void head( final Node node, final int depth ) {
42
      if( node instanceof final TextNode textNode ) {
43
        final var parent = node.parentNode();
44
        final var name = parent == null ? "root" : parent.nodeName();
45
46
        if( !("pre".equalsIgnoreCase( name ) ||
47
          "code".equalsIgnoreCase( name ) ||
48
          "kbd".equalsIgnoreCase( name ) ||
49
          "var".equalsIgnoreCase( name ) ||
50
          "tt".equalsIgnoreCase( name )) ) {
51
          // Calling getWholeText() will return newlines, which must be kept
52
          // to ensure that preformatted text maintains its formatting.
53
          textNode.text( replace( textNode.getWholeText(), LIGATURES ) );
54
        }
55
      }
56
    }
57
58
    @Override
59
    public void tail( final Node node, final int depth ) {
60
    }
61
  };
62
63
  @Override
64
  public Document fromJsoup( final org.jsoup.nodes.Document in ) {
65
    assert in != null;
66
67
    final var out = DocumentParser.newDocument();
68
    final org.jsoup.nodes.DocumentType doctype = in.documentType();
69
70
    if( doctype != null ) {
71
      out.appendChild(
72
        sDomImplementation.createDocumentType(
73
          doctype.name(),
74
          doctype.publicId(),
75
          doctype.systemId()
76
        )
77
      );
78
    }
79
80
    out.setXmlStandalone( true );
81
    in.traverse( LIGATURE_VISITOR );
82
    convert( in, out );
83
84
    return out;
85
  }
86
}
187
A src/main/java/com/keenwrite/dom/DocumentParser.java
1
package com.keenwrite.dom;
2
3
import org.w3c.dom.*;
4
import org.xml.sax.InputSource;
5
import org.xml.sax.SAXException;
6
7
import javax.xml.parsers.DocumentBuilder;
8
import javax.xml.parsers.DocumentBuilderFactory;
9
import javax.xml.transform.Transformer;
10
import javax.xml.transform.TransformerException;
11
import javax.xml.transform.TransformerFactory;
12
import javax.xml.transform.dom.DOMSource;
13
import javax.xml.transform.stream.StreamResult;
14
import javax.xml.xpath.XPath;
15
import javax.xml.xpath.XPathExpression;
16
import javax.xml.xpath.XPathExpressionException;
17
import javax.xml.xpath.XPathFactory;
18
import java.io.IOException;
19
import java.io.InputStream;
20
import java.io.StringReader;
21
import java.io.StringWriter;
22
import java.nio.file.Path;
23
import java.util.HashMap;
24
import java.util.Map;
25
import java.util.function.Consumer;
26
27
import static com.keenwrite.events.StatusEvent.clue;
28
import static java.nio.charset.StandardCharsets.UTF_8;
29
import static javax.xml.transform.OutputKeys.*;
30
import static javax.xml.xpath.XPathConstants.NODESET;
31
32
/**
33
 * Responsible for initializing an XML parser.
34
 */
35
public class DocumentParser {
36
  private static final String LOAD_EXTERNAL_DTD =
37
    "http://apache.org/xml/features/nonvalidating/load-external-dtd";
38
39
  /**
40
   * Caches {@link XPathExpression}s to avoid re-compiling.
41
   */
42
  private static final Map<String, XPathExpression> sXpaths = new HashMap<>();
43
44
  private static final DocumentBuilderFactory sDocumentFactory;
45
  private static DocumentBuilder sDocumentBuilder;
46
  public static DOMImplementation sDomImplementation;
47
  public static Transformer sTransformer;
48
  private static final XPath sXpath = XPathFactory.newInstance().newXPath();
49
50
  static {
51
    sDocumentFactory = DocumentBuilderFactory.newInstance();
52
53
    sDocumentFactory.setValidating( false );
54
    sDocumentFactory.setAttribute( LOAD_EXTERNAL_DTD, false );
55
    sDocumentFactory.setNamespaceAware( true );
56
    sDocumentFactory.setIgnoringComments( true );
57
    sDocumentFactory.setIgnoringElementContentWhitespace( true );
58
59
    try {
60
      sDocumentBuilder = sDocumentFactory.newDocumentBuilder();
61
      sDomImplementation = sDocumentBuilder.getDOMImplementation();
62
      sTransformer = TransformerFactory.newInstance().newTransformer();
63
64
      sTransformer.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
65
      sTransformer.setOutputProperty( METHOD, "xml" );
66
      sTransformer.setOutputProperty( INDENT, "no" );
67
      sTransformer.setOutputProperty( ENCODING, UTF_8.toString() );
68
    } catch( final Exception ex ) {
69
      clue( ex );
70
    }
71
  }
72
73
  /**
74
   * Use the {@code static} constants and methods, not an instance, at least
75
   * until an iterable sub-interface is written.
76
   */
77
  private DocumentParser() {}
78
79
  public static Document newDocument() {
80
    return sDocumentBuilder.newDocument();
81
  }
82
83
  /**
84
   * Creates a new document object model based on the given XML document
85
   * string. This will return an empty document if the document could not
86
   * be parsed.
87
   *
88
   * @param xml The document text to convert into a DOM.
89
   * @return The DOM that represents the given XML data.
90
   */
91
  public static Document parse( final String xml ) {
92
    final var input = new InputSource();
93
94
    try( final var reader = new StringReader( xml ) ) {
95
      input.setEncoding( UTF_8.toString() );
96
      input.setCharacterStream( reader );
97
      return sDocumentBuilder.parse( input );
98
    } catch( final Exception ex ) {
99
      clue( ex );
100
      return sDocumentBuilder.newDocument();
101
    }
102
  }
103
104
  public static Document parse( final InputStream doc )
105
    throws IOException, SAXException {
106
    return sDocumentBuilder.parse( doc );
107
  }
108
109
  /**
110
   * Allows an operation to be applied for every node in the document that
111
   * matches a given tag name pattern.
112
   *
113
   * @param document Document to traverse.
114
   * @param xpath    Document elements to find via {@link XPath} expression.
115
   * @param consumer The consumer to call for each matching document node.
116
   */
117
  public static void walk(
118
    final Document document, final String xpath,
119
    final Consumer<Node> consumer ) {
120
    assert document != null;
121
    assert consumer != null;
122
123
    try {
124
      final var expr = lookupXPathExpression( xpath );
125
      final var nodes = (NodeList) expr.evaluate( document, NODESET );
126
127
      if( nodes != null ) {
128
        for( int i = 0, len = nodes.getLength(); i < len; i++ ) {
129
          consumer.accept( nodes.item( i ) );
130
        }
131
      }
132
    } catch( final Exception ex ) {
133
      clue( ex );
134
    }
135
  }
136
137
  public static Node createMeta(
138
    final Document document, final Map.Entry<String, String> entry ) {
139
    final var node = document.createElement( "meta" );
140
    node.setAttribute( "name", entry.getKey() );
141
    node.setAttribute( "content", entry.getValue() );
142
143
    return node;
144
  }
145
146
  public static String toString( final Document xhtml ) {
147
    try( final var writer = new StringWriter() ) {
148
      final var domSource = new DOMSource( xhtml );
149
      final var result = new StreamResult( writer );
150
151
      sTransformer.transform( domSource, result );
152
      return writer.toString();
153
    } catch( final Exception ex ) {
154
      clue( ex );
155
      return "";
156
    }
157
  }
158
159
  public static String transform( final Element root )
160
    throws IOException, TransformerException {
161
    try( final var writer = new StringWriter() ) {
162
      sTransformer.transform(
163
        new DOMSource( root ), new StreamResult( writer ) );
164
      return writer.toString();
165
    }
166
  }
167
168
  /**
169
   * Remove whitespace, comments, and XML/DOCTYPE declarations to make
170
   * processing work with ConTeXt.
171
   *
172
   * @param path The SVG file to process.
173
   * @throws Exception The file could not be processed.
174
   */
175
  public static void sanitize( final Path path )
176
    throws Exception {
177
    final var file = path.toFile();
178
179
    sTransformer.transform(
180
      new DOMSource( sDocumentBuilder.parse( file ) ), new StreamResult( file )
181
    );
182
  }
183
184
  /**
185
   * Adorns the given document with {@code html}, {@code head}, and
186
   * {@code body} elements.
187
   *
188
   * @param html The document to decorate.
189
   * @return A document with a typical HTML structure.
190
   */
191
  public static String decorate( final String html ) {
192
    return
193
      "<html><head><title> </title></head><body>" + html + "</body></html>";
194
  }
195
196
  private static XPathExpression lookupXPathExpression( final String xpath ) {
197
    return sXpaths.computeIfAbsent( xpath, k -> {
198
      try {
199
        return sXpath.compile( xpath );
200
      } catch( final XPathExpressionException ex ) {
201
        clue( ex );
202
        return null;
203
      }
204
    } );
205
  }
206
}
1207
M src/main/java/com/keenwrite/events/DocumentChangedEvent.java
44
import org.jsoup.nodes.Document;
55
6
import static com.keenwrite.util.MurmurHash.hash32;
7
import static java.lang.System.currentTimeMillis;
8
96
/**
107
 * Collates information about an HTML document that has changed.
118
 */
129
public class DocumentChangedEvent implements AppEvent {
13
  private static final int SEED = (int) currentTimeMillis();
14
1510
  private final String mText;
1611
1712
  /**
18
   * Hash the document so subscribers are only informed upon changes.
13
   * Hash document (as plain text) so subscribers are notified upon changes.
1914
   */
2015
  private static int sHash;
...
4035
   */
4136
  public static void fireDocumentChangedEvent( final Document html ) {
37
    // Hashing the document text ignores caret position changes.
4238
    final var text = html.wholeText();
43
    final var hash = hash32( text, 0, text.length(), SEED );
39
    final var hash = text.hashCode();
4440
4541
    if( hash != sHash ) {
M src/main/java/com/keenwrite/preferences/PreferencesController.java
176176
          Setting.of( title( KEY_TYPESET_CONTEXT_CLEAN ),
177177
                      booleanProperty( KEY_TYPESET_CONTEXT_CLEAN ) )
178
        ),
179
        Group.of(
180
          get( KEY_TYPESET_TYPOGRAPHY ),
181
          Setting.of( label( KEY_TYPESET_TYPOGRAPHY_QUOTES ) ),
182
          Setting.of( title( KEY_TYPESET_TYPOGRAPHY_QUOTES ),
183
                      booleanProperty( KEY_TYPESET_TYPOGRAPHY_QUOTES ) )
178184
        )
179185
      ),
M src/main/java/com/keenwrite/preferences/Workspace.java
118118
    entry( KEY_LANGUAGE_LOCALE, asLocaleProperty( LOCALE_DEFAULT ) ),
119119
120
    entry( KEY_TYPESET_CONTEXT_CLEAN, asBooleanProperty(true) ),
120
    entry( KEY_TYPESET_CONTEXT_CLEAN, asBooleanProperty( true ) ),
121121
    entry( KEY_TYPESET_CONTEXT_THEMES_PATH, asFileProperty( USER_DIRECTORY ) ),
122
    entry( KEY_TYPESET_CONTEXT_THEME_SELECTION, asStringProperty( "boschet" ) )
122
    entry( KEY_TYPESET_CONTEXT_THEME_SELECTION, asStringProperty( "boschet" ) ),
123
    entry( KEY_TYPESET_TYPOGRAPHY_QUOTES, asBooleanProperty( true ) )
123124
    //@formatter:on
124125
  );
M src/main/java/com/keenwrite/preferences/WorkspaceKeys.java
8888
  public static final Key KEY_TYPESET_CONTEXT_THEME_SELECTION = key( KEY_TYPESET_CONTEXT_THEMES, "selection" );
8989
  public static final Key KEY_TYPESET_CONTEXT_CLEAN = key( KEY_TYPESET_CONTEXT, "clean" );
90
  public static final Key KEY_TYPESET_TYPOGRAPHY = key( KEY_TYPESET, "typography" );
91
  public static final Key KEY_TYPESET_TYPOGRAPHY_QUOTES = key( KEY_TYPESET_TYPOGRAPHY, "quotes" );
9092
  //@formatter:on
9193
D src/main/java/com/keenwrite/preview/DomConverter.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.preview;
3
4
import org.jsoup.helper.W3CDom;
5
import org.jsoup.nodes.Node;
6
import org.jsoup.nodes.TextNode;
7
import org.jsoup.select.NodeVisitor;
8
import org.w3c.dom.DOMImplementation;
9
import org.w3c.dom.Document;
10
11
import javax.xml.parsers.DocumentBuilder;
12
import javax.xml.parsers.DocumentBuilderFactory;
13
import java.util.LinkedHashMap;
14
import java.util.Map;
15
16
import static com.keenwrite.events.StatusEvent.clue;
17
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
18
19
/**
20
 * Responsible for converting JSoup document object model (DOM) to a W3C DOM.
21
 * Provides a lighter implementation than the superclass by overriding the
22
 * {@link #fromJsoup(org.jsoup.nodes.Document)} method to reuse factories,
23
 * builders, and implementations.
24
 */
25
final class DomConverter extends W3CDom {
26
  /**
27
   * Retain insertion order using an instance of {@link LinkedHashMap} so
28
   * that ligature substitution uses longer ligatures ahead of shorter
29
   * ligatures. The word "ruffian" should use the "ffi" ligature, not the "ff"
30
   * ligature.
31
   */
32
  private static final Map<String, String> LIGATURES = new LinkedHashMap<>();
33
34
  static {
35
    LIGATURES.put( "ffi", "\uFB03" );
36
    LIGATURES.put( "ffl", "\uFB04" );
37
    LIGATURES.put( "ff", "\uFB00" );
38
    LIGATURES.put( "fi", "\uFB01" );
39
    LIGATURES.put( "fl", "\uFB02" );
40
  }
41
42
  private static final NodeVisitor LIGATURE_VISITOR = new NodeVisitor() {
43
    @Override
44
    public void head( final Node node, final int depth ) {
45
      if( node instanceof final TextNode textNode ) {
46
        final var parent = node.parentNode();
47
        final var name = parent == null ? "root" : parent.nodeName();
48
49
        if( !("pre".equalsIgnoreCase( name ) ||
50
          "code".equalsIgnoreCase( name ) ||
51
          "tt".equalsIgnoreCase( name )) ) {
52
          // Calling getWholeText() will return newlines, which must be kept
53
          // to ensure that preformatted text maintains its formatting.
54
          textNode.text( replace( textNode.getWholeText(), LIGATURES ) );
55
        }
56
      }
57
    }
58
59
    @Override
60
    public void tail( final Node node, final int depth ) {
61
    }
62
  };
63
64
  private static final DocumentBuilderFactory DOCUMENT_FACTORY;
65
  private static DocumentBuilder DOCUMENT_BUILDER;
66
  private static DOMImplementation DOM_IMPL;
67
68
  static {
69
    DOCUMENT_FACTORY = DocumentBuilderFactory.newInstance();
70
    DOCUMENT_FACTORY.setNamespaceAware( true );
71
72
    try {
73
      DOCUMENT_BUILDER = DOCUMENT_FACTORY.newDocumentBuilder();
74
      DOM_IMPL = DOCUMENT_BUILDER.getDOMImplementation();
75
    } catch( final Exception ex ) {
76
      clue( ex );
77
    }
78
  }
79
80
  @Override
81
  public Document fromJsoup( final org.jsoup.nodes.Document in ) {
82
    assert in != null;
83
    assert DOCUMENT_BUILDER != null;
84
    assert DOM_IMPL != null;
85
86
    final var out = DOCUMENT_BUILDER.newDocument();
87
    final org.jsoup.nodes.DocumentType doctype = in.documentType();
88
89
    if( doctype != null ) {
90
      out.appendChild(
91
        DOM_IMPL.createDocumentType(
92
          doctype.name(),
93
          doctype.publicId(),
94
          doctype.systemId()
95
        )
96
      );
97
    }
98
99
    out.setXmlStandalone( true );
100
    in.traverse( LIGATURE_VISITOR );
101
    convert( in, out );
102
103
    return out;
104
  }
105
}
1061
M src/main/java/com/keenwrite/preview/HtmlPanel.java
22
package com.keenwrite.preview;
33
4
import com.keenwrite.dom.DocumentConverter;
45
import com.keenwrite.ui.adapters.DocumentAdapter;
56
import javafx.beans.property.BooleanProperty;
...
8485
  }
8586
86
  private static final DomConverter CONVERTER = new DomConverter();
87
  private static final DocumentConverter CONVERTER = new DocumentConverter();
8788
  private static final XhtmlNamespaceHandler XNH = new XhtmlNamespaceHandler();
8889
M src/main/java/com/keenwrite/preview/HtmlPreview.java
4747
   */
4848
  private static final String HTML_STYLESHEET =
49
    "<link rel='stylesheet' href='%s'>";
49
    "<link rel='stylesheet' href='%s'/>";
5050
5151
  private static final String HTML_BASE =
52
    "<base href='%s'>";
52
    "<base href='%s'/>";
5353
5454
  /**
...
6969
    """
7070
      <!doctype html>
71
      <html lang='%s'><head><title> </title><meta charset='utf-8'>
71
      <html lang='%s'><head><title> </title><meta charset='utf-8'/>
7272
      %s%s<style>body{font-family:'%s';font-size: %dpx;}</style>%s</head><body>
7373
      """;
M src/main/java/com/keenwrite/preview/SvgRasterizer.java
1414
import org.w3c.dom.Element;
1515
16
import javax.xml.transform.Transformer;
17
import javax.xml.transform.TransformerFactory;
18
import javax.xml.transform.dom.DOMSource;
19
import javax.xml.transform.stream.StreamResult;
2016
import java.awt.*;
2117
import java.awt.image.BufferedImage;
22
import java.io.*;
18
import java.io.File;
19
import java.io.IOException;
20
import java.io.InputStream;
21
import java.io.StringReader;
2322
import java.net.URI;
2423
import java.nio.file.Path;
2524
import java.text.NumberFormat;
2625
import java.text.ParseException;
2726
27
import static com.keenwrite.dom.DocumentParser.transform;
2828
import static com.keenwrite.events.StatusEvent.clue;
2929
import static com.keenwrite.preview.HighQualityRenderingHints.RENDERING_HINTS;
3030
import static java.awt.image.BufferedImage.TYPE_INT_RGB;
31
import static java.nio.charset.StandardCharsets.UTF_8;
3231
import static java.text.NumberFormat.getIntegerInstance;
33
import static javax.xml.transform.OutputKeys.*;
3432
import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
3533
import static org.apache.batik.transcoder.image.ImageTranscoder.KEY_PIXEL_UNIT_TO_MILLIMETER;
...
6058
  private static final SAXSVGDocumentFactory FACTORY_DOM =
6159
    new SAXSVGDocumentFactory( getXMLParserClassName() );
62
63
  private static final TransformerFactory FACTORY_TRANSFORM =
64
    TransformerFactory.newInstance();
65
66
  private static final Transformer sTransformer;
67
68
  static {
69
    Transformer t;
70
71
    try {
72
      t = FACTORY_TRANSFORM.newTransformer();
73
      t.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
74
      t.setOutputProperty( METHOD, "xml" );
75
      t.setOutputProperty( INDENT, "no" );
76
      t.setOutputProperty( ENCODING, UTF_8.name() );
77
    } catch( final Exception ignored ) {
78
      t = null;
79
    }
80
81
    sTransformer = t;
82
  }
8360
8461
  private static final NumberFormat INT_FORMAT = getIntegerInstance();
...
315292
   * element to a string.
316293
   *
317
   * @param e The DOM node to convert to a string.
294
   * @param root The DOM node to convert to a string.
318295
   * @return The DOM node as an escaped, plain text string.
319296
   */
320
  public static String toSvg( final Element e ) {
321
    try( final var writer = new StringWriter() ) {
322
      sTransformer.transform( new DOMSource( e ), new StreamResult( writer ) );
323
      return writer.toString().replaceAll( "xmlns=\"\" ", "" );
297
  public static String toSvg( final Element root ) {
298
    try {
299
      return transform( root ).replaceAll( "xmlns=\"\" ", "" );
324300
    } catch( final Exception ex ) {
325301
      clue( ex );
M src/main/java/com/keenwrite/processors/HtmlPreviewProcessor.java
1212
 */
1313
public final class HtmlPreviewProcessor extends ExecutorProcessor<String> {
14
1514
  /**
1615
   * There is only one preview panel.
M src/main/java/com/keenwrite/processors/ProcessorFactory.java
3434
    // HTML preview pane.
3535
    //
36
    // Otherwise, bolt on a processor that--after the interpolation and
36
    // Otherwise, bolt on a processor that---after the interpolation and
3737
    // substitution phase, which includes text strings or R code---will
3838
    // generate HTML or plain Markdown. HTML has a few output formats:
3939
    // with embedded SVG representing formulas, or without any conversion
4040
    // to SVG. Without conversion would require client-side rendering of
4141
    // math (such as using the JavaScript-based KaTeX engine).
4242
    final var successor = context.isExportFormat( NONE )
43
      ? createHtmlPreviewProcessor()
43
      ? createHtmlPreviewProcessor( context )
4444
      : context.isExportFormat( XHTML_TEX )
4545
      ? createXhtmlProcessor( context )
4646
      : context.isExportFormat( APPLICATION_PDF )
4747
      ? createPdfProcessor( context )
48
      : createIdentityProcessor();
48
      : createIdentityProcessor( context );
4949
5050
    final var processor = switch( context.getFileType() ) {
...
7474
   * @return An instance of {@link Processor} that performs no processing.
7575
   */
76
  private Processor<String> createIdentityProcessor() {
76
  @SuppressWarnings( "unused" )
77
  private Processor<String> createIdentityProcessor(
78
    final ProcessorContext ignored ) {
7779
    return IDENTITY;
7880
  }
7981
8082
  /**
8183
   * Instantiates a new {@link Processor} that passes an incoming HTML
8284
   * string to a user interface widget that can render HTML as a web page.
8385
   *
8486
   * @return An instance of {@link Processor} that forwards HTML for display.
8587
   */
86
  private Processor<String> createHtmlPreviewProcessor() {
88
  @SuppressWarnings( "unused" )
89
  private Processor<String> createHtmlPreviewProcessor(
90
    final ProcessorContext ignored ) {
8791
    return new HtmlPreviewProcessor( getPreviewPane() );
8892
  }
M src/main/java/com/keenwrite/processors/XhtmlProcessor.java
22
package com.keenwrite.processors;
33
4
import com.keenwrite.dom.DocumentParser;
45
import com.keenwrite.preferences.Key;
56
import com.keenwrite.preferences.Workspace;
67
import com.keenwrite.ui.heuristics.WordCounter;
8
import com.whitemagicsoftware.keenquotes.Converter;
79
import javafx.beans.property.StringProperty;
8
import org.jsoup.nodes.Document;
10
import org.w3c.dom.Document;
911
10
import javax.xml.parsers.DocumentBuilderFactory;
11
import javax.xml.transform.TransformerFactory;
12
import javax.xml.transform.dom.DOMSource;
13
import javax.xml.transform.stream.StreamResult;
1412
import java.io.FileNotFoundException;
1513
import java.nio.file.Path;
1614
import java.util.Locale;
1715
import java.util.Map;
18
import java.util.Map.Entry;
1916
import java.util.regex.Pattern;
2017
2118
import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE;
19
import static com.keenwrite.dom.DocumentParser.*;
2220
import static com.keenwrite.events.StatusEvent.clue;
2321
import static com.keenwrite.io.HttpFacade.httpGet;
2422
import static com.keenwrite.preferences.WorkspaceKeys.*;
2523
import static com.keenwrite.processors.text.TextReplacementFactory.replace;
2624
import static com.keenwrite.util.ProtocolScheme.getProtocol;
25
import static com.whitemagicsoftware.keenquotes.Converter.CHARS;
26
import static com.whitemagicsoftware.keenquotes.ParserFactory.ParserType.PARSER_XML;
2727
import static java.lang.String.format;
2828
import static java.lang.String.valueOf;
2929
import static java.nio.file.Files.copy;
3030
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
3131
import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS;
3232
import static java.util.regex.Pattern.compile;
33
import static javax.xml.transform.OutputKeys.INDENT;
34
import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION;
35
import static org.jsoup.Jsoup.parse;
36
import static org.jsoup.nodes.Document.OutputSettings.Syntax;
3733
3834
/**
39
 * Responsible for making an HTML document complete by wrapping it with html
35
 * Responsible for making an XHTML document complete by wrapping it with html
4036
 * and body elements. This doesn't have to be super-efficient because it's
4137
 * not run in real-time.
4238
 */
4339
public final class XhtmlProcessor extends ExecutorProcessor<String> {
4440
  private final static Pattern BLANK =
4541
    compile( "\\p{Blank}", UNICODE_CHARACTER_CLASS );
42
43
  private final static Converter sTypographer =
44
    new Converter( lex -> clue( lex.toString() ), CHARS, PARSER_XML );
4645
4746
  private final ProcessorContext mContext;
...
6665
    clue( "Main.status.typeset.xhtml" );
6766
68
    final var doc = parse( html );
69
    setMetaData( doc );
70
    doc.outputSettings().syntax( Syntax.xml );
67
    try {
68
      final var doc = DocumentParser.parse( decorate( html ) );
69
      setMetaData( doc );
7170
72
    for( final var img : doc.getElementsByTag( "img" ) ) {
73
      try {
74
        final var imageFile = exportImage( img.attr( "src" ) );
71
      walk( doc, "//img", node -> {
72
        try {
73
          final var attrs = node.getAttributes();
7574
76
        img.attr( "src", imageFile.toString() );
77
      } catch( final Exception ex ) {
78
        clue( ex );
79
      }
75
          if( attrs != null ) {
76
            final var attr = attrs.getNamedItem( "src" );
77
78
            if( attr != null ) {
79
              final var imageFile = exportImage( attr.getTextContent() );
80
81
              attr.setTextContent( imageFile.toString() );
82
            }
83
          }
84
        } catch( final Exception ex ) {
85
          clue( ex );
86
        }
87
      } );
88
89
      final var document = DocumentParser.toString( doc );
90
91
      return curl() ? sTypographer.apply( document ) : document;
92
    } catch( final Exception ex ) {
93
      clue( ex );
8094
    }
8195
82
    return doc.html();
96
    return html;
8397
  }
8498
8599
  /**
86100
   * Applies the metadata fields to the document.
87101
   *
88102
   * @param doc The document to adorn with metadata.
89103
   */
90104
  private void setMetaData( final Document doc ) {
91
    doc.title( getTitle() );
92
93105
    final var metadata = createMetaData( doc );
94
    final var head = doc.head();
95
    metadata.entrySet().forEach( entry -> head.append( createMeta( entry ) ) );
96
  }
97106
98
  private String createMeta( final Entry<String, String> entry ) {
99
    return format(
100
      "<meta name='%s' content='%s'>", entry.getKey(), entry.getValue()
107
    walk( doc, "/html/head", node ->
108
      metadata.entrySet()
109
              .forEach( entry -> node.appendChild( createMeta( doc, entry ) ) )
101110
    );
111
    walk( doc, "/html/head/title", node -> node.setTextContent( title() ) );
102112
  }
103113
...
110120
  private Map<String, String> createMetaData( final Document doc ) {
111121
    return Map.of(
112
      "author", getAuthor(),
113
      "byline", getByline(),
114
      "address", getAddress(),
115
      "phone", getPhone(),
116
      "email", getEmail(),
117
      "count", getWordCount( doc ),
118
      "keywords", getKeywords(),
119
      "copyright", getCopyright(),
120
      "date", getDate()
122
      "author", author(),
123
      "byline", byLine(),
124
      "address", address(),
125
      "phone", phone(),
126
      "email", email(),
127
      "count", wordCount( doc ),
128
      "keywords", keywords(),
129
      "copyright", copyright(),
130
      "date", date()
121131
    );
122132
  }
...
149159
      // Strip comments, superfluous whitespace, DOCTYPE, and XML declarations.
150160
      if( mediaType.isSvg() ) {
151
        sanitize( imageFile );
161
        DocumentParser.sanitize( imageFile );
152162
      }
153163
    }
...
183193
184194
    return imageFile;
185
  }
186
187
  /**
188
   * Remove whitespace, comments, and XML/DOCTYPE declarations to make
189
   * processing work with ConTeXt.
190
   *
191
   * @param path The SVG file to process.
192
   * @throws Exception The file could not be processed.
193
   */
194
  private void sanitize( final Path path )
195
    throws Exception {
196
    final var file = path.toFile();
197
198
    final var dbf = DocumentBuilderFactory.newInstance();
199
    dbf.setIgnoringComments( true );
200
    dbf.setIgnoringElementContentWhitespace( true );
201
202
    final var db = dbf.newDocumentBuilder();
203
    final var document = db.parse( file );
204
205
    final var tf = TransformerFactory.newInstance();
206
    final var transformer = tf.newTransformer();
207
208
    final var source = new DOMSource( document );
209
    final var result = new StreamResult( file );
210
    transformer.setOutputProperty( OMIT_XML_DECLARATION, "yes" );
211
    transformer.setOutputProperty( INDENT, "no" );
212
    transformer.transform( source, result );
213195
  }
214196
...
235217
  }
236218
237
  private Locale getLocale() { return getWorkspace().getLocale(); }
219
  private Locale locale() { return getWorkspace().getLocale(); }
238220
239
  private String getTitle() {
221
  private String title() {
240222
    return resolve( KEY_DOC_TITLE );
241223
  }
242224
243
  private String getAuthor() {
225
  private String author() {
244226
    return resolve( KEY_DOC_AUTHOR );
245227
  }
246228
247
  private String getByline() {
229
  private String byLine() {
248230
    return resolve( KEY_DOC_BYLINE );
249231
  }
250232
251
  private String getAddress() {
233
  private String address() {
252234
    return resolve( KEY_DOC_ADDRESS ).replaceAll( "\n", "\\\\\\break{}" );
253235
  }
254236
255
  private String getPhone() {
237
  private String phone() {
256238
    return resolve( KEY_DOC_PHONE );
257239
  }
258240
259
  private String getEmail() {
241
  private String email() {
260242
    return resolve( KEY_DOC_EMAIL );
261243
  }
262244
263
  private String getWordCount( final Document doc ) {
264
    final var text = doc.wholeText();
265
    final var wordCounter = WordCounter.create( getLocale() );
266
    return valueOf( wordCounter.countWords( text ) );
245
  private String wordCount( final Document doc ) {
246
    final var sb = new StringBuilder( 65536 * 10 );
247
248
    walk(
249
      doc,
250
      "//*[normalize-space( text() ) != '']",
251
      node -> sb.append( node.getTextContent() )
252
    );
253
254
    return valueOf( WordCounter.create( locale() ).count( sb.toString() ) );
267255
  }
268256
269
  private String getKeywords() {
257
  private String keywords() {
270258
    return resolve( KEY_DOC_KEYWORDS );
271259
  }
272260
273
  private String getCopyright() {
261
  private String copyright() {
274262
    return resolve( KEY_DOC_COPYRIGHT );
275263
  }
276264
277
  private String getDate() {
265
  private String date() {
278266
    return resolve( KEY_DOC_DATE );
267
  }
268
269
  /**
270
   * Answers whether straight quotation marks should be curled.
271
   *
272
   * @return {@code false} to prevent curling straight quotes.
273
   */
274
  private boolean curl() {
275
    return getWorkspace().toBoolean( KEY_TYPESET_TYPOGRAPHY_QUOTES );
279276
  }
280277
M src/main/java/com/keenwrite/processors/markdown/BaseMarkdownProcessor.java
1111
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
1212
import com.vladsch.flexmark.ext.tables.TablesExtension;
13
import com.vladsch.flexmark.ext.typographic.TypographicExtension;
1413
import com.vladsch.flexmark.html.HtmlRenderer;
1514
import com.vladsch.flexmark.parser.Parser;
1615
import com.vladsch.flexmark.util.ast.IParse;
1716
import com.vladsch.flexmark.util.ast.IRender;
1817
import com.vladsch.flexmark.util.ast.Node;
1918
import com.vladsch.flexmark.util.misc.Extension;
2019
2120
import java.util.ArrayList;
2221
import java.util.List;
23
24
import static com.keenwrite.ExportFormat.APPLICATION_PDF;
25
import static com.vladsch.flexmark.ext.typographic.TypographicExtension.ENABLE_SMARTS;
2622
2723
/**
...
3935
    super( successor );
4036
41
    // Disable emdash, endash, and ellipses conversion for PDF exports. The
42
    // typesetting software will perform the appropriate styling. This allows
43
    // manuscripts to include verbatim hyphens, for example.
4437
    final var builder = Parser.builder();
45
    builder.set( ENABLE_SMARTS, !context.isExportFormat( APPLICATION_PDF ) );
46
4738
    final var extensions = createExtensions( context );
4839
    mParser = builder.extensions( extensions ).build();
4940
    mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
5041
  }
5142
5243
  /**
53
   * Instantiates a number of extensions to be applied when parsing. These
54
   * are typically typographic extensions that convert characters into
55
   * HTML entities.
44
   * Instantiates a number of extensions to be applied when parsing.
5645
   *
5746
   * @param context The context that subclasses use to configure custom
...
6857
    extensions.add( TablesExtension.create() );
6958
    extensions.add( FencedDivExtension.create() );
70
71
    if( !context.isExportFormat( APPLICATION_PDF ) ) {
72
      extensions.add( TypographicExtension.create() );
73
    }
7459
7560
    return extensions;
D src/main/java/com/keenwrite/quotes/SmartQuotes.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.quotes;
3
4
import java.io.BufferedReader;
5
import java.io.InputStreamReader;
6
import java.util.AbstractMap.SimpleEntry;
7
import java.util.ArrayList;
8
import java.util.Map;
9
import java.util.function.Function;
10
import java.util.regex.Pattern;
11
12
import static java.util.Arrays.binarySearch;
13
import static java.util.Collections.sort;
14
15
/**
16
 * Responsible for converting straight quotes into smart quotes. This must be
17
 * used on plain text. The class will not parse HTML, TeX, or non-English text.
18
 */
19
public class SmartQuotes {
20
21
  /**
22
   * The main regex captures all words that contain an apostrophe. The terms
23
   * inner, outer, began, and ended define where the apostrophes can be found
24
   * in a particular word. The following text contains 3 word matches against
25
   * the "inner" pattern:
26
   *
27
   * <p>
28
   * 'Janes' said, ''E'll be spooky, Sam's son with the jack-o'-lantern!',"
29
   * said the O'Mally twins'---y'know---ghosts in unison.'
30
   * </p>
31
   */
32
  private static final Map<String, Pattern> PATTERNS = Map.ofEntries(
33
    // @formatter:off
34
    createEntry( "inner", "(?<![\\p{L}'])(?:\\p{L}+')+\\p{L}+(?![\\p{L}'])" ),
35
    createEntry( "began", "(?<!\\p{L})(?:'\\p{L}+)+(?![\\p{L}'])" ),
36
    createEntry( "ended", "(?<![\\p{L}'])(?:\\p{L}+')+(?!\\p{L})" ),
37
    createEntry( "outer", "(?<!\\p{L})'\\p{L}+'(?!\\p{L})" ),
38
    createEntry( "years", "'(?=\\d{2}s?)" ),
39
    createEntry( "+ings", "[\\p{L}]{2,}in'\\s?" ),
40
    createEntry( "prime", "((-?[0-9]\\d*(\\.\\d+)?)\\\\?'\\s?(-?[0-9]\\d*(\\.\\d+)?)\\\\?\")|((-?[0-9]\\d*(\\.\\d+)?)(''|\")\\s?(x|×)\\s?(-?[0-9]\\d*(\\.\\d+)?)(''|\"))|((-?[0-9]\\d*(\\.\\d+)?)'')" ),
41
    createEntry( "texop", "``" ),
42
    createEntry( "texcl", "''" ),
43
    createEntry( "white", "(?!\\s+)\"|\"(?!\\s+)" ),
44
    createEntry( "slash", "\\\\\"" )
45
    // @formatter:on
46
  );
47
48
  private static SimpleEntry<String, Pattern> createEntry(
49
    final String key, final String regex ) {
50
    return new SimpleEntry<>( key, Pattern.compile( regex ) );
51
  }
52
53
  /**
54
   * Left single quote replacement text.
55
   */
56
  private static final String QUOTE_SINGLE_LEFT = "&lsquo;";
57
58
  /**
59
   * Right single quote replacement text.
60
   */
61
  private static final String QUOTE_SINGLE_RIGHT = "&rsquo;";
62
63
  /**
64
   * Left double quote replacement text.
65
   */
66
  private static final String QUOTE_DOUBLE_LEFT = "&ldquo;";
67
68
  /**
69
   * Right double quote replacement text.
70
   */
71
  private static final String QUOTE_DOUBLE_RIGHT = "&rdquo;";
72
73
  /**
74
   * Apostrophe replacement text.
75
   */
76
  private static final String APOSTROPHE = "&apos;";
77
78
  /**
79
   * Prime replacement text.
80
   */
81
  private static final String SINGLE_PRIME = "&prime;";
82
83
  /**
84
   * Double prime replacement text.
85
   */
86
  private static final String DOUBLE_PRIME = "&Prime;";
87
88
  /**
89
   * Temporary single quote marker near end of Unicode private use area.
90
   */
91
  private static final String SQ = "\uF8FE";
92
93
  /**
94
   * Temporary double quote marker near end of Unicode private use area.
95
   */
96
  private static final String DQ = "\uF8FD";
97
98
  private final Map<String, String[]> CONTRACTIONS = Map.ofEntries(
99
    load( "inner" ),
100
    load( "began" ),
101
    load( "ended" ),
102
    load( "outer" ),
103
    load( "verbs" )
104
  );
105
106
  public SmartQuotes() {
107
  }
108
109
  /**
110
   * Replaces straight single and double quotes with curly quotes or primes,
111
   * depending on the context.
112
   *
113
   * @param text The text that may contain straight single or double quotes.
114
   * @return All single and double quotes replaced with typographically
115
   * correct quotation marks.
116
   */
117
  public String replace( String text ) {
118
    // Replace known contractions.
119
    text = contractions( text );
120
121
    // Replace miscellaneous verb contractions.
122
    text = verbs( text );
123
124
    // Replace primes and double-primes (e.g., 5'4").
125
    text = primes( text );
126
127
    // Replace decade contractions.
128
    text = decades( text );
129
130
    // Replace contractions of words ending in "ing" (e.g., washin').
131
    text = suffixes( text );
132
133
    // Replace double backticks.
134
    text = backticks( text );
135
136
    // Unescape straight double quotes.
137
    text = escapes( text );
138
139
    return text;
140
  }
141
142
  /**
143
   * Replaces all strings in the given text that match the given pattern,
144
   * provided the functor answers {@code true} to the matched regex.
145
   *
146
   * @param text    The text to perform a replacement.
147
   * @param pattern The regular expression pattern to match.
148
   * @param filter  Controls whether a text replacement is made.
149
   * @return The given text with matching patterns replaced, conditionally.
150
   */
151
  private String replace( final String text,
152
                          final Pattern pattern,
153
                          final Function<String, Boolean> filter,
154
                          final Function<String, String> subst ) {
155
    final var sb = new StringBuilder( text.length() * 2 );
156
    final var matcher = pattern.matcher( text );
157
158
    while( matcher.find() ) {
159
      final var match = matcher.group( 0 );
160
      if( filter.apply( match ) ) {
161
        matcher.appendReplacement( sb, subst.apply( match ) );
162
      }
163
    }
164
165
    matcher.appendTail( sb );
166
    return sb.toString();
167
  }
168
169
  /**
170
   * Convenience method that always performs string replacement upon a match,
171
   * unconditionally.
172
   */
173
  private String apostrophize( final String text, final Pattern pattern ) {
174
    return apostrophize( text, pattern, ( match ) -> true );
175
  }
176
177
  private String apostrophize( final String text, final String pattern ) {
178
    return apostrophize( text, PATTERNS.get( pattern ) );
179
  }
180
181
  private String decades( final String text ) {
182
    return apostrophize( text, "years" );
183
  }
184
185
  private String suffixes( final String text ) {
186
    return apostrophize( text, "+ings" );
187
  }
188
189
  /**
190
   * Convenience method that replaces each straight quote in the given {@code
191
   * text} that passes through the given filter with an {@link #APOSTROPHE}.
192
   */
193
  private String apostrophize(
194
    final String text,
195
    final Pattern pattern,
196
    final Function<String, Boolean> filter ) {
197
    return replace(
198
      text,
199
      pattern,
200
      filter,
201
      ( match ) -> match.replaceAll( "'", APOSTROPHE ) );
202
  }
203
204
  private String contractions( String text ) {
205
    final var elements = new String[]{"inner", "began", "ended", "outer"};
206
207
    for( final var item : elements ) {
208
      final var pattern = PATTERNS.get( item );
209
      final var contractions = CONTRACTIONS.get( item );
210
211
      text = apostrophize(
212
        text,
213
        pattern,
214
        ( match ) -> binarySearch( contractions, match.toLowerCase() ) >= 0
215
      );
216
    }
217
218
    return text;
219
  }
220
221
  /**
222
   * Replaces verb endings, such as 'll and 've, with words not explicitly
223
   * listed as contractions in the dictionary sources.
224
   *
225
   * @param text The text to replace.
226
   * @return The given text with matching patterns replaced.
227
   */
228
  private String verbs( String text ) {
229
    for( final var contraction : CONTRACTIONS.get( "verbs" ) ) {
230
      final var pattern = Pattern.compile( "[\\p{L}]+" + contraction );
231
      text = apostrophize( text, pattern );
232
    }
233
234
    return text;
235
  }
236
237
  private String primes( final String text ) {
238
    System.out.println( "REPLACE: " + text);
239
    return replace(
240
      text,
241
      PATTERNS.get( "prime" ),
242
      ( match ) -> true,
243
      ( match ) -> match.replaceAll( "''", DOUBLE_PRIME )
244
                        .replaceAll( "\"", DOUBLE_PRIME )
245
                        .replaceAll( "'", SINGLE_PRIME )
246
                        .replaceAll( "\\\\", "" )
247
    );
248
  }
249
250
  /**
251
   * Replace all double backticks with opening double quote.
252
   */
253
  private String backticks( String text ) {
254
    final var sb = new StringBuilder( text.length() * 2 );
255
    final var opening = PATTERNS.get( "texop" );
256
    final var opener = opening.matcher( text );
257
    var count = 0;
258
259
    while( opener.find() ) {
260
      count++;
261
      opener.appendReplacement( sb, QUOTE_DOUBLE_LEFT );
262
    }
263
264
    opener.appendTail( sb );
265
266
    if( count > 0 ) {
267
      text = sb.toString();
268
      sb.setLength( 0 );
269
270
      final var closing = PATTERNS.get( "texcl" );
271
      final var closer = closing.matcher( text );
272
      while( count > 0 && closer.find() ) {
273
        count--;
274
        closer.appendReplacement( sb, QUOTE_DOUBLE_RIGHT );
275
      }
276
277
      closer.appendTail( sb );
278
    }
279
280
    return sb.toString();
281
  }
282
283
  private String escapes( final String text ) {
284
    return replace(
285
      text,
286
      PATTERNS.get( "slash" ),
287
      ( match ) -> true,
288
      ( match ) -> match.replaceAll( "\\\\", "" )
289
    );
290
  }
291
292
  /**
293
   * Reads the list of words containing contractions.
294
   */
295
  @SuppressWarnings( "SameParameterValue" )
296
  private SimpleEntry<String, String[]> load( final String prefix ) {
297
    // Allocate enough elements to hold all the contractions.
298
    final var result = new ArrayList<String>( 1024 );
299
300
    try( final var in = openResource( prefix + ".txt" ) ) {
301
      for( String line; ((line = in.readLine()) != null); ) {
302
        result.add( line );
303
      }
304
305
      sort( result );
306
    } catch( final Exception ex ) {
307
      throw new RuntimeException( ex );
308
    }
309
310
    return new SimpleEntry<>( prefix, result.toArray( new String[ 0 ] ) );
311
  }
312
313
  private BufferedReader openResource( final String filename ) {
314
    final var in = getClass().getResourceAsStream( filename );
315
    assert in != null;
316
317
    return new BufferedReader( new InputStreamReader( in ) );
318
  }
319
}
3201
M src/main/java/com/keenwrite/ui/dialogs/ThemePicker.java
1414
import java.io.FileInputStream;
1515
import java.io.IOException;
16
import java.io.InputStreamReader;
17
import java.nio.charset.StandardCharsets;
1618
import java.nio.file.Path;
1719
import java.util.Properties;
...
2527
import static com.keenwrite.util.FileWalker.walk;
2628
import static java.lang.Math.max;
29
import static java.nio.charset.StandardCharsets.UTF_8;
2730
import static org.codehaus.plexus.util.StringUtils.abbreviate;
2831
...
148151
  }
149152
150
  private Properties read( final Path file ) throws IOException {
153
  /**
154
   * Reads an instance of {@link Properties} from the given {@link Path} using
155
   * {@link StandardCharsets#UTF_8} encoding.
156
   *
157
   * @param path The fully qualified path to the file.
158
   * @return The path to the file to read.
159
   * @throws IOException Could not open the file for reading.
160
   */
161
  private Properties read( final Path path ) throws IOException {
151162
    final var properties = new Properties();
152163
153
    try( final var in = new FileInputStream( file.toFile() ) ) {
164
    try( final var in = new InputStreamReader(
165
      new FileInputStream( path.toFile() ), UTF_8 ) ) {
154166
      properties.load( in );
155167
    }
M src/main/java/com/keenwrite/ui/heuristics/DocumentStatistics.java
55
import com.keenwrite.preferences.Workspace;
66
import com.keenwrite.preview.HtmlPanel;
7
import com.keenwrite.util.MurmurHash;
87
import com.whitemagicsoftware.wordcount.TokenizerException;
98
import javafx.beans.property.IntegerProperty;
...
1615
import javafx.scene.control.TableView;
1716
import org.greenrobot.eventbus.Subscribe;
18
import org.jsoup.Jsoup;
1917
2018
import static com.keenwrite.events.Bus.register;
...
6866
6967
  /**
70
   * Called when the hashcode for the current document changes. This happens
68
   * Called when the hash code for the current document changes. This happens
7169
   * when non-collapsable-whitespace is added to the document. When the
72
   * document is sent to {@link HtmlPanel} for rendering, the parsed
73
   * {@link Jsoup} document is converted to text. If that text differs
74
   * (using {@link MurmurHash}), then this method is called. The implication
75
   * is that all variables and executable statements have been replaced.
76
   * An event bus subscriber is used so that text processing occurs outside
77
   * of the UI processing threads.
70
   * document is sent to {@link HtmlPanel} for rendering, the parsed document
71
   * is converted to text. If that text differs in its hash code, then this
72
   * method is called. The implication is that all variables and executable
73
   * statements have been replaced. An event bus subscriber is used so that
74
   * text processing occurs outside of the UI processing threads.
7875
   *
7976
   * @param event Container for the document text that has changed.
8077
   */
8178
  @Subscribe
8279
  public void handle( final DocumentChangedEvent event ) {
8380
    try {
8481
      runLater( () -> {
8582
        mItems.clear();
8683
        final var document = event.getDocument();
87
        final var wordCount = mWordCounter.countWords(
84
        final var wordCount = mWordCounter.count(
8885
          document, ( k, count ) -> {
8986
            // Generate statistics for words that occur thrice or more.
M src/main/java/com/keenwrite/ui/heuristics/WordCounter.java
3333
   * @return The total number of words in the document.
3434
   */
35
  public int countWords( final String document ) {
36
    return countWords( document, ( k, count ) -> {} );
35
  public int count( final String document ) {
36
    return count( document, ( k, count ) -> {} );
3737
  }
3838
3939
  /**
4040
   * Counts the number of unique words in the document.
4141
   *
4242
   * @param document The document to tally.
4343
   * @param consumer The action to take for each unique word/count pair.
4444
   * @return The total number of words in the document.
4545
   */
46
  public int countWords(
46
  public int count(
4747
    final String document, final BiConsumer<String, Integer> consumer ) {
4848
    final var tokens = mTokenizer.tokenize( document );
D src/main/java/com/keenwrite/util/MurmurHash.java
1
package com.keenwrite.util;
2
3
/**
4
 * The MurmurHash3 algorithm was created by Austin Appleby and placed in the
5
 * public domain. This Java port was authored by Yonik Seeley and also placed
6
 * into the public domain. The author hereby disclaims copyright to this
7
 * source code.
8
 * <p>
9
 * This produces exactly the same hash values as the final C++ version and is
10
 * thus suitable for producing the same hash values across platforms.
11
 * <p>
12
 * The 32-bit x86 version of this hash should be the fastest variant for
13
 * relatively short keys like ids. Using {@link #hash32} is a
14
 * good choice for longer strings or returning more than 32 hashed bits.
15
 * <p>
16
 * The x86 and x64 versions do not produce the same results because
17
 * algorithms are optimized for their respective platforms.
18
 * <p>
19
 * Code clean-up by White Magic Software, Ltd.
20
 * </p>
21
 */
22
public final class MurmurHash {
23
  /**
24
   * Returns the 32-bit x86-optimized hash of the UTF-8 bytes of the String
25
   * without actually encoding the string to a temporary buffer. This is over
26
   * twice as fast as hashing the result of {@link String#getBytes()}.
27
   */
28
  @SuppressWarnings( "unused" )
29
  public static int hash32( CharSequence data, int offset, int len, int seed ) {
30
    final int c1 = 0xcc9e2d51;
31
    final int c2 = 0x1b873593;
32
33
    int h1 = seed;
34
35
    int pos = offset;
36
    int end = offset + len;
37
    int k1 = 0;
38
    int k2;
39
    int shift = 0;
40
    int bits;
41
    int nBytes = 0;   // length in UTF8 bytes
42
43
    while( pos < end ) {
44
      int code = data.charAt( pos++ );
45
      if( code < 0x80 ) {
46
        k2 = code;
47
        bits = 8;
48
      }
49
      else if( code < 0x800 ) {
50
        k2 = (0xC0 | (code >> 6))
51
          | ((0x80 | (code & 0x3F)) << 8);
52
        bits = 16;
53
      }
54
      else if( code < 0xD800 || code > 0xDFFF || pos >= end ) {
55
        // we check for pos>=end to encode an unpaired surrogate as 3 bytes.
56
        k2 = (0xE0 | (code >> 12))
57
          | ((0x80 | ((code >> 6) & 0x3F)) << 8)
58
          | ((0x80 | (code & 0x3F)) << 16);
59
        bits = 24;
60
      }
61
      else {
62
        // surrogate pair
63
        // int utf32 = pos < end ? (int) data.charAt(pos++) : 0;
64
        int utf32 = data.charAt( pos++ );
65
        utf32 = ((code - 0xD7C0) << 10) + (utf32 & 0x3FF);
66
        k2 = (0xff & (0xF0 | (utf32 >> 18)))
67
          | ((0x80 | ((utf32 >> 12) & 0x3F))) << 8
68
          | ((0x80 | ((utf32 >> 6) & 0x3F))) << 16
69
          | (0x80 | (utf32 & 0x3F)) << 24;
70
        bits = 32;
71
      }
72
73
      k1 |= k2 << shift;
74
75
      // int used_bits = 32 - shift;  // how many bits of k2 were used in k1.
76
      // int unused_bits = bits - used_bits; //  (bits-(32-shift)) ==
77
      // bits+shift-32  == bits-newshift
78
79
      shift += bits;
80
      if( shift >= 32 ) {
81
        // mix after we have a complete word
82
83
        k1 *= c1;
84
        k1 = (k1 << 15) | (k1 >>> 17);  // ROTL32(k1,15);
85
        k1 *= c2;
86
87
        h1 ^= k1;
88
        h1 = (h1 << 13) | (h1 >>> 19);  // ROTL32(h1,13);
89
        h1 = h1 * 5 + 0xe6546b64;
90
91
        shift -= 32;
92
        // unfortunately, java won't let you shift 32 bits off, so we need to
93
        // check for 0
94
        if( shift != 0 ) {
95
          k1 = k2 >>> (bits - shift);   // bits used == bits - newshift
96
        }
97
        else {
98
          k1 = 0;
99
        }
100
        nBytes += 4;
101
      }
102
103
    } // inner
104
105
    // handle tail
106
    if( shift > 0 ) {
107
      nBytes += shift >> 3;
108
      k1 *= c1;
109
      k1 = (k1 << 15) | (k1 >>> 17);  // ROTL32(k1,15);
110
      k1 *= c2;
111
      h1 ^= k1;
112
    }
113
114
    // finalization
115
    h1 ^= nBytes;
116
117
    // fmix(h1);
118
    h1 ^= h1 >>> 16;
119
    h1 *= 0x85ebca6b;
120
    h1 ^= h1 >>> 13;
121
    h1 *= 0xc2b2ae35;
122
    h1 ^= h1 >>> 16;
123
124
    return h1;
125
  }
126
}
1271
M src/main/resources/com/keenwrite/messages.properties
4646
workspace.typeset.context.clean.desc=Delete ancillary files after an unsuccessful export.
4747
workspace.typeset.context.clean.title=Purge
48
workspace.typeset.typography=Typography
49
workspace.typeset.typography.quotes=Quotation Marks
50
workspace.typeset.typography.quotes.desc=Convert straight quotes into curly quotes and primes.
51
workspace.typeset.typography.quotes.title=Curl
4852
4953
workspace.r=R
A src/test/java/com/keenwrite/flexmark/ParserTest.java
1
/* Copyright 2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.flexmark;
3
4
import com.keenwrite.processors.markdown.extensions.fences.FencedDivExtension;
5
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
6
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
7
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
8
import com.vladsch.flexmark.ext.tables.TablesExtension;
9
import com.vladsch.flexmark.html.HtmlRenderer;
10
import com.vladsch.flexmark.parser.Parser;
11
import com.vladsch.flexmark.util.data.MutableDataSet;
12
import com.vladsch.flexmark.util.misc.Extension;
13
import org.junit.jupiter.api.Test;
14
15
import java.util.ArrayList;
16
import java.util.List;
17
18
import static org.junit.jupiter.api.Assertions.assertEquals;
19
20
/**
21
 * Test that basic styles for conversion exports as expected.
22
 */
23
public class ParserTest {
24
25
  @Test
26
  void test_Conversion_InlineStyles_ExportedAsHtml() {
27
    final var md = "*emphasis* _emphasis_ **strong**";
28
29
    final var extensions = createExtensions();
30
    final var options = new MutableDataSet();
31
    final var parser = Parser
32
      .builder( options )
33
      .extensions( extensions )
34
      .build();
35
    final var renderer = HtmlRenderer
36
      .builder( options )
37
      .extensions( extensions )
38
      .build();
39
40
    final var document = parser.parse( md );
41
    final var actual = renderer.render( document );
42
    final var expected =
43
      "<p><em>emphasis</em> <em>emphasis</em> <strong>strong</strong></p>\n";
44
45
    assertEquals( expected, actual );
46
  }
47
48
  private List<Extension> createExtensions() {
49
    final var extensions = new ArrayList<Extension>();
50
51
    extensions.add( DefinitionExtension.create() );
52
    extensions.add( StrikethroughSubscriptExtension.create() );
53
    extensions.add( SuperscriptExtension.create() );
54
    extensions.add( TablesExtension.create() );
55
    extensions.add( FencedDivExtension.create() );
56
57
    return extensions;
58
  }
59
}
160
D src/test/java/com/keenwrite/quotes/SmartQuotesTest.java
1
/* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */
2
package com.keenwrite.quotes;
3
4
import org.junit.jupiter.api.Disabled;
5
import org.junit.jupiter.api.Test;
6
7
import java.io.BufferedReader;
8
import java.io.IOException;
9
import java.io.InputStreamReader;
10
11
import static org.junit.jupiter.api.Assertions.assertEquals;
12
import static org.junit.jupiter.api.Assertions.assertNotNull;
13
14
/**
15
 * Test that English straight quotes are converted to curly quotes and
16
 * apostrophes.
17
 */
18
public class SmartQuotesTest {
19
  @Disabled
20
  @SuppressWarnings( "unused" )
21
  public void test_Parse_StraightQuotes_CurlyQuotes() throws IOException {
22
    final var fixer = new SmartQuotes();
23
24
    try( final var reader = openResource( "smartypants.txt" ) ) {
25
      String line;
26
      String testLine = "";
27
      String expected = "";
28
29
      while( ((line = reader.readLine()) != null) ) {
30
        if( line.startsWith( "#" ) || line.isBlank() ) { continue; }
31
32
        // Read the first line of the couplet.
33
        if( testLine.isBlank() ) {
34
          testLine = line;
35
          continue;
36
        }
37
38
        // Read the second line of the couplet.
39
        if( expected.isBlank() ) {
40
          expected = line;
41
        }
42
43
        final var actual = fixer.replace( testLine );
44
        assertEquals(expected, actual);
45
46
        testLine = "";
47
        expected = "";
48
      }
49
    }
50
  }
51
52
  @SuppressWarnings( "SameParameterValue" )
53
  private BufferedReader openResource( final String filename ) {
54
    final var is = getClass().getResourceAsStream( filename );
55
    assertNotNull( is );
56
57
    return new BufferedReader( new InputStreamReader( is ) );
58
  }
59
}
601
D src/test/java/com/keenwrite/tex/TeXRasterization.java
1
/*
2
 * Copyright 2020-2021 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.tex;
29
30
import com.whitemagicsoftware.tex.DefaultTeXFont;
31
import com.whitemagicsoftware.tex.TeXEnvironment;
32
import com.whitemagicsoftware.tex.TeXFormula;
33
import com.whitemagicsoftware.tex.TeXLayout;
34
import com.whitemagicsoftware.tex.graphics.AbstractGraphics2D;
35
import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
36
import com.whitemagicsoftware.tex.graphics.SvgGraphics2D;
37
import org.apache.batik.transcoder.TranscoderException;
38
import org.junit.jupiter.api.Test;
39
import org.xml.sax.SAXException;
40
41
import javax.imageio.ImageIO;
42
import javax.xml.parsers.DocumentBuilderFactory;
43
import javax.xml.parsers.ParserConfigurationException;
44
import java.awt.image.BufferedImage;
45
import java.io.ByteArrayInputStream;
46
import java.io.File;
47
import java.io.IOException;
48
import java.nio.file.Path;
49
import java.text.ParseException;
50
51
import static com.keenwrite.preview.SvgRasterizer.*;
52
import static java.lang.System.getProperty;
53
import static org.junit.jupiter.api.Assertions.assertEquals;
54
55
/**
56
 * Test that TeX rasterization produces a readable image.
57
 */
58
public class TeXRasterization {
59
  private static final String LOAD_EXTERNAL_DTD =
60
      "http://apache.org/xml/features/nonvalidating/load-external-dtd";
61
62
  private static final String EQUATION =
63
      "G_{\\mu \\nu} = \\frac{8 \\pi G}{c^4} T_{{\\mu \\nu}}";
64
65
  private static final String DIR_TEMP = getProperty( "java.io.tmpdir" );
66
67
  private static final long FILESIZE = 12364;
68
69
  /**
70
   * Test that an equation can be converted to a raster image and the
71
   * final raster image size corresponds to the input equation. This is
72
   * a simple way to verify that the rasterization process is correct,
73
   * albeit if any aspect of the SVG algorithm changes (such as padding
74
   * around the equation), it will cause this test to fail, which is a bit
75
   * misleading.
76
   */
77
  @Test
78
  public void test_Rasterize_SimpleFormula_CorrectImageSize()
79
    throws IOException, ParseException, TranscoderException {
80
    final var g = new SvgGraphics2D();
81
    drawGraphics( g );
82
    verifyImage( rasterizeString( g.toString() ) );
83
  }
84
85
  /**
86
   * Test that an SVG document object model can be parsed and rasterized into
87
   * an image.
88
   */
89
  @Test
90
  public void getTest_SvgDomGraphics2D_InputElement_OutputRasterizedImage()
91
    throws ParserConfigurationException, IOException, SAXException,
92
    ParseException, TranscoderException {
93
    final var g = new SvgGraphics2D();
94
    drawGraphics( g );
95
96
    final var expectedSvg = g.toString();
97
    final var bytes = expectedSvg.getBytes();
98
99
    final var dbf = DocumentBuilderFactory.newInstance();
100
    dbf.setFeature( LOAD_EXTERNAL_DTD, false );
101
    dbf.setNamespaceAware( false );
102
    final var builder = dbf.newDocumentBuilder();
103
104
    final var doc = builder.parse( new ByteArrayInputStream( bytes ) );
105
    final var actualSvg = toSvg( doc.getDocumentElement() );
106
107
    verifyImage( rasterizeString( actualSvg ) );
108
  }
109
110
  /**
111
   * Test that an SVG image from a DOM element can be rasterized.
112
   *
113
   * @throws IOException Could not write the image.
114
   */
115
  @Test
116
  public void test_SvgDomGraphics2D_InputDom_OutputRasterizedImage()
117
    throws IOException, ParseException, TranscoderException {
118
    final var g = new SvgDomGraphics2D();
119
    drawGraphics( g );
120
121
    final var dom = g.toDom();
122
123
    verifyImage( rasterize( dom ) );
124
  }
125
126
  /**
127
   * Asserts that the given image matches an expected file size.
128
   *
129
   * @param image The image to check against the file size.
130
   * @throws IOException Could not write the image.
131
   */
132
  private void verifyImage( final BufferedImage image ) throws IOException {
133
    final var file = export( image, "dom.png" );
134
    assertEquals( FILESIZE, file.length() );
135
  }
136
137
  /**
138
   * Creates an SVG string for the default equation and font size.
139
   */
140
  private void drawGraphics( final AbstractGraphics2D g ) {
141
    final var size = 100f;
142
    final var texFont = new DefaultTeXFont( size );
143
    final var env = new TeXEnvironment( texFont );
144
    g.scale( size, size );
145
146
    final var formula = new TeXFormula( EQUATION );
147
    final var box = formula.createBox( env );
148
    final var layout = new TeXLayout( box, size );
149
150
    g.initialize( layout.getWidth(), layout.getHeight() );
151
    box.draw( g, layout.getX(), layout.getY() );
152
  }
153
154
  @SuppressWarnings("SameParameterValue")
155
  private File export( final BufferedImage image, final String filename )
156
      throws IOException {
157
    final var path = Path.of( DIR_TEMP, filename );
158
    final var file = path.toFile();
159
    ImageIO.write( image, "png", file );
160
    file.deleteOnExit();
161
    return file;
162
  }
163
}
1641
A src/test/java/com/keenwrite/tex/TeXRasterizationTest.java
1
/*
2
 * Copyright 2020-2021 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.keenwrite.tex;
29
30
import com.whitemagicsoftware.tex.DefaultTeXFont;
31
import com.whitemagicsoftware.tex.TeXEnvironment;
32
import com.whitemagicsoftware.tex.TeXFormula;
33
import com.whitemagicsoftware.tex.TeXLayout;
34
import com.whitemagicsoftware.tex.graphics.AbstractGraphics2D;
35
import com.whitemagicsoftware.tex.graphics.SvgDomGraphics2D;
36
import com.whitemagicsoftware.tex.graphics.SvgGraphics2D;
37
import org.apache.batik.transcoder.TranscoderException;
38
import org.junit.jupiter.api.Test;
39
import org.xml.sax.SAXException;
40
41
import javax.imageio.ImageIO;
42
import java.awt.image.BufferedImage;
43
import java.io.ByteArrayInputStream;
44
import java.io.File;
45
import java.io.IOException;
46
import java.nio.file.Path;
47
import java.text.ParseException;
48
49
import static com.keenwrite.dom.DocumentParser.parse;
50
import static com.keenwrite.preview.SvgRasterizer.*;
51
import static java.lang.System.getProperty;
52
import static org.junit.jupiter.api.Assertions.assertEquals;
53
54
/**
55
 * Test that TeX rasterization produces a readable image.
56
 */
57
public class TeXRasterizationTest {
58
  private static final String EQUATION =
59
    "G_{\\mu \\nu} = \\frac{8 \\pi G}{c^4} T_{{\\mu \\nu}}";
60
61
  private static final String DIR_TEMP = getProperty( "java.io.tmpdir" );
62
63
  private static final long FILESIZE = 12364;
64
65
  /**
66
   * Test that an equation can be converted to a raster image and the
67
   * final raster image size corresponds to the input equation. This is
68
   * a simple way to verify that the rasterization process is correct,
69
   * albeit if any aspect of the SVG algorithm changes (such as padding
70
   * around the equation), it will cause this test to fail, which is a bit
71
   * misleading.
72
   */
73
  @Test
74
  public void test_Rasterize_SimpleFormula_CorrectImageSize()
75
    throws IOException, ParseException, TranscoderException {
76
    final var g = new SvgGraphics2D();
77
78
    drawGraphics( g );
79
    verifyImage( rasterizeString( g.toString() ) );
80
  }
81
82
  /**
83
   * Test that an SVG document object model can be parsed and rasterized into
84
   * an image.
85
   */
86
  @Test
87
  public void getTest_SvgDomGraphics2D_InputElement_OutputRasterizedImage()
88
    throws IOException, SAXException, ParseException, TranscoderException {
89
    final var g = new SvgGraphics2D();
90
    drawGraphics( g );
91
92
    final var expectedSvg = g.toString();
93
    final var bytes = expectedSvg.getBytes();
94
    final var doc = parse( new ByteArrayInputStream( bytes ) );
95
    final var actualSvg = toSvg( doc.getDocumentElement() );
96
97
    verifyImage( rasterizeString( actualSvg ) );
98
  }
99
100
  /**
101
   * Test that an SVG image from a DOM element can be rasterized.
102
   *
103
   * @throws IOException Could not write the image.
104
   */
105
  @Test
106
  public void test_SvgDomGraphics2D_InputDom_OutputRasterizedImage()
107
    throws IOException, ParseException, TranscoderException {
108
    final var g = new SvgDomGraphics2D();
109
110
    drawGraphics( g );
111
    verifyImage( rasterize( g.toDom() ) );
112
  }
113
114
  /**
115
   * Asserts that the given image matches an expected file size.
116
   *
117
   * @param image The image to check against the file size.
118
   * @throws IOException Could not write the image.
119
   */
120
  private void verifyImage( final BufferedImage image ) throws IOException {
121
    final var file = export( image, "dom.png" );
122
    assertEquals( FILESIZE, file.length() );
123
  }
124
125
  /**
126
   * Creates an SVG string for the default equation and font size.
127
   */
128
  private void drawGraphics( final AbstractGraphics2D g ) {
129
    final var size = 100f;
130
    final var texFont = new DefaultTeXFont( size );
131
    final var env = new TeXEnvironment( texFont );
132
    g.scale( size, size );
133
134
    final var formula = new TeXFormula( EQUATION );
135
    final var box = formula.createBox( env );
136
    final var layout = new TeXLayout( box, size );
137
138
    g.initialize( layout.getWidth(), layout.getHeight() );
139
    box.draw( g, layout.getX(), layout.getY() );
140
  }
141
142
  @SuppressWarnings( "SameParameterValue" )
143
  private File export( final BufferedImage image, final String filename )
144
    throws IOException {
145
    final var path = Path.of( DIR_TEMP, filename );
146
    final var file = path.toFile();
147
    ImageIO.write( image, "png", file );
148
    file.deleteOnExit();
149
    return file;
150
  }
151
}
1152
D src/test/resources/com/keenwrite/quotes/smartypants.txt
1
# ########################################################################
2
# Decades
3
# ########################################################################
4
The Roaring '20s had the best music, no?
5
The Roaring &apos;20s had the best music, no?
6
7
Took place in '04, yes'm!
8
Took place in &apos;04, yes&apos;m!
9
10
# ########################################################################
11
# Inside contractions (no leading/trailing apostrophes)
12
# ########################################################################
13
I don't like it: I love's it!
14
I don&apos;t like it: I love&apos;s it!
15
16
We'd've thought that pancakes'll be sweeter there.
17
We&apos;d&apos;ve thought that pancakes&apos;ll be sweeter there.
18
19
She'd be coming o'er when the horse'd gone to pasture...
20
She&apos;d be coming o&apos;er when the horse&apos;d gone to pasture...
21
22
# ########################################################################
23
# Beginning contractions (leading apostrophes)
24
# ########################################################################
25
'Twas and 'tis whate'er lay 'twixt dawn and dusk 'n River Styx.
26
&apos;Twas and &apos;tis whate&apos;er lay &apos;twixt dawn and dusk &apos;n River Styx.
27
28
# ########################################################################
29
# Ending contractions (trailing apostrophes)
30
# ########################################################################
31
Didn' get th' message.
32
Didn&apos; get th&apos; message.
33
34
Namsayin', y'know what I'ma sayin'?
35
Namsayin&apos;, y&apos;know what I&apos;ma sayin&apos;?
36
37
# ########################################################################
38
# Outside contractions (leading and trailing, no middle)
39
# ########################################################################
40
Salt 'n' vinegar, fish-'n'-chips, sugar 'n' spice!
41
Salt &apos;n&apos; vinegar, fish-&apos;n&apos;-chips, sugar &apos;n&apos; spice!
42
43
# ########################################################################
44
# Primes (single, double)
45
# ########################################################################
46
She stood 5\'7\".
47
She stood 5&prime;7&Prime;.
48
49
# No space after the feet sign.
50
It's 4'11" away.
51
It&apos;s 4&prime;11&Prime; away.
52
53
Alice's friend is 6'3" tall.
54
Alice&apos;s friend is 6&prime;3&Prime; tall.
55
56
Bob's table is 5'' × 4''.
57
Bob&apos;s table is 5&Prime; × 4&Prime;.
58
59
What's this -5.5'' all about?
60
What&apos;s this -5.5&Prime; all about?
61
62
+7.9'' is weird.
63
+7.9&Prime; is weird.
64
65
Foolscap? Naw, I use 11.5"x14.25" paper!
66
Foolscap? Naw, I use 11.5&Prime;x14.25&Prime; paper!
67
68
An angular measurement, 3° 5' 30" means 3 degs, 5 arcmins, and 30 arcsecs.
69
An angular measurement, 3° 5&prime; 30&Prime; means 3 degs, 5 arcmins, and 30 arcsecs.
70
71
# ########################################################################
72
# Backticks (left and right double quotes)
73
# ########################################################################
74
``I am Sam''
75
&ldquo;I am Sam&rdquo;
76
77
``Sam's away today''
78
&ldquo;Sam&apos;s away today&rdquo;
79
80
``Sam's gone!
81
&ldquo;Sam&apos;s gone!
82
83
``5'10" tall 'e was!''
84
&ldquo;5&prime;10&Prime; tall &apos;e was!&rdquo;
85
86
# ########################################################################
87
# Consecutive quotes
88
# ########################################################################
89
"'I'm trouble.'"
90
&ldquo;&lsquo;I&apos;m trouble.&rsquo;&rdquo;
91
92
'"Trouble's my name."'
93
&lsquo;&ldquo;Trouble&apos;s my name.&ldquo;&lsquo;
94
95
# ########################################################################
96
# Escaped quotes
97
# ########################################################################
98
\"What?\"
99
&ldquo;What?&rdquo;
100
101
# ########################################################################
102
# Double quotes
103
# ########################################################################
104
"I am Sam"
105
&ldquo;I am Sam&rdquo;
106
107
"...even better!"
108
&ldquo;...even better!&rdquo;
109
110
"It was so," said he.
111
&ldquo;It was so,&rdquo; said he.
112
113
"She said, 'Llamas'll languish, they'll--
114
&ldquo;She said, &lsquo;Llamas&apos;ll languish, they&apos;ll--
115
116
With "air quotes" in the middle.
117
With &ldquo;air quotes&rdquo; in the middle.
118
119
With--"air quotes"--and dashes.
120
With--&ldquo;air quotes&rdquo;--and dashes.
121
122
"Not "quite" what you expected?"
123
&ldquo;Not &ldquo;quite&rdquo; what you expected?&rdquo;
124
125
# ########################################################################
126
# Nested quotations
127
# ########################################################################
128
"'Here I am,' said Sam"
129
&ldquo;&lsquo;Here I am,&rsquo; said Sam&rdquo;
130
131
'"Here I am," said Sam'
132
&lsquo;&ldquo;Here I am,&rdquo;, said Sam&rsquo;
133
134
'Hello, "Dr. Brown," what's your real name?'
135
&lsquo;Hello, &ldquo;Dr. Brown,&rdquo; what's your real name?&rsquo;
136
137
"'Twas, t'wasn't thy name, 'twas it?" said Jim "the Barber" Brown.
138
&ldquo;&apos;Twas, t&apos;wasn&apos;t thy name, &apos;twas it?&rdquo; said Jim &ldquo;the Barber&rdquo; Brown.
139
140
# ########################################################################
141
# Single quotes
142
# ########################################################################
143
'I am Sam'
144
&lsquo;I am Sam&rsquo;
145
146
'It was so,' said he.
147
&lsquo;It was so,&rsquo; said he.
148
149
'...even better!'
150
&lsquo;...even better!&rsquo;
151
152
With 'quotes' in the middle.
153
With &lsquo;quotes&rsquo; in the middle.
154
155
With--'imaginary'--dashes.
156
With--&lsquo;imaginary&rsquo;--dashes.
157
158
'Not 'quite' what you expected?'
159
&lsquo;Not &lsquo;quite&rsquo; what you expected?&rsquo;
160
161
''Cause I don't like it, 's why,' said Pat.
162
&lsquo;&apos;Cause I don't like it, &apos;s why,&rsquo; said Pat.
163
164
'It's a beautiful day!'
165
&lsquo;It&apos;s a beautiful day!&rsquo;
166
167
'He said, 'Thinkin'.'
168
&lsquo;He said, &lsquo;Thinkin&rsquo;.&rsquo;
169
170
# ########################################################################
171
# Possessives
172
# ########################################################################
173
Sam's Sams' and the Ross's roses' thorns were prickly.
174
Sam&apos;s Sams&apos; and the Ross&apos;s roses&apos; thorns were prickly.
175
176
# ########################################################################
177
# Mixed
178
# ########################################################################
179
"I heard she said, 'That's Sam's'," said the Sams' cat.
180
&ldquo;I heard she said, &lsquo;That&apos;s Sam&apos;s&rsquo;,&rdquo; said the Sams&apos; cat.
181
182
"'Janes' said, ''E'll be spooky, Sam's son with the jack-o'-lantern!'" said the O'Mally twins'---y'know---ghosts in unison.
183
&ldquo;&lsquo;Janes&apos; said, &lsquo;&apos;E&apos;ll be spooky, Sam&apos;s son with the jack-o&apos;-lantern!&rsquo;&rdquo; said the O&apos;Mally twins&apos;---y&apos;know---ghosts in unison.
184
185
'He's at Sams'
186
&lsquo;He&apos; at Sams&rsquo;
187
188
\"Hello!\"
189
&ldquo;Hello!&rdquo;
190
191
ma'am
192
ma&apos;am
193
194
'Twas midnight
195
&apos;Twas midnight
196
197
\"Hello,\" said the spider. \"'Shelob' is my name.\"
198
&ldquo;Hello,&rdquo; said the spider. &ldquo;&lsquo;Shelob&rsquo; is my name.&rdquo;
199
200
'A', 'B', and 'C' are letters.
201
&lsquo;A&rsquo; &lsquo;B&rsquo; and &lsquo;C&rsquo; are letters.
202
203
'Oak,' 'elm,' and 'beech' are names of trees. So is 'pine.'
204
&lsquo;Oak,&rsquo; &lsquo;elm,&rsquo; and &lsquo;beech&rsquo; are names of trees. So is &lsquo;pine.&rsquo;
205
206
'He said, \"I want to go.\"' Were you alive in the 70's?
207
&lsquo;He said, &ldquo;I want to go.&rdquo;&rsquo; Were you alive in the 70&apos;s?
208
209
\"That's a 'magic' sock.\"
210
&ldquo;That&apos;s a &lsquo;magic&rsquo; sock.&rdquo;
211
212
Website! Company Name, Inc. (\"Company Name\" or \"Company\") recommends reading the following terms and conditions, carefully:
213
Website! Company Name, Inc. (&ldquo;Company Name&rdquo; or &ldquo;Company&rdquo;) recommends reading the following terms and conditions, carefully:
214
215
Website! Company Name, Inc. ('Company Name' or 'Company') recommends reading the following terms and conditions, carefully:
216
Website! Company Name, Inc. (&lsquo;Company Name&rsquo; or &lsquo;Company&rsquo;) recommends reading the following terms and conditions, carefully:
217
218
Workin' hard
219
Workin&apos; hard
220
221
'70s are my favorite numbers,' she said.
222
&lsquo;70s are my favorite numbers,&rsquo; she said.
223
224
'70s fashion was weird.
225
&apos;70s fashion was weird.
226
227
12\" record, 5'10\" height
228
12&Prime; record, 5&prime;10&Prime; height
229
230
Model \"T2000\"
231
Model &ldquo;T2000&rdquo;
232
233
iPad 3's battery life is not great.
234
iPad 3&apos;s battery life is not great.
235
236
Book 'em, Danno. Rock 'n' roll. 'Cause 'twas the season.
237
Book &apos;em, Danno. Rock &apos;n&apos; roll. &apos;Cause &apos;twas the season.
238
239
'85 was a good year. (The entire '80s were.)
240
&apos;85 was a good year. (The entire &apos;80s were.)
241
2421