| 3 | 3 | #  |
| 4 | 4 | |
| 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. | |
| 6 | 6 | |
| 7 | 7 | ## Download |
| 50 | 50 | |
| 51 | 51 | dependencies { |
| 52 | def v_junit = '5.7.1' | |
| 52 | def v_junit = '5.7.2' | |
| 53 | 53 | def v_flexmark = '0.62.2' |
| 54 | def v_jackson = '2.12.2' | |
| 54 | def v_jackson = '2.12.3' | |
| 55 | 55 | def v_batik = '1.14' |
| 56 | 56 | def v_wheatsheaf = '2.0.1' |
| 57 | 57 | |
| 58 | 58 | // JavaFX |
| 59 | 59 | implementation 'org.controlsfx:controlsfx:11.1.0' |
| 60 | 60 | implementation 'org.fxmisc.richtext:richtextfx:0.10.6' |
| 61 | 61 | implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3' |
| 62 | implementation 'com.miglayout:miglayout-javafx:5.2' | |
| 62 | implementation 'com.miglayout:miglayout-javafx:11.0' | |
| 63 | 63 | implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.8.0' |
| 64 | 64 | |
| ... | ||
| 81 | 81 | implementation "com.fasterxml.jackson.core:jackson-annotations:${v_jackson}" |
| 82 | 82 | implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:${v_jackson}" |
| 83 | implementation 'org.yaml:snakeyaml:1.27' | |
| 83 | implementation 'org.yaml:snakeyaml:1.29' | |
| 84 | 84 | |
| 85 | 85 | // XML |
| ... | ||
| 117 | 117 | implementation 'org.greenrobot:eventbus:3.2.0' |
| 118 | 118 | |
| 119 | // Configuration: Update Workspace to use Jackson, instead could shave ~800kb | |
| 119 | // TODO: Update Workspace config to use Jackson to shave ~800kb | |
| 120 | 120 | implementation 'org.apache.commons:commons-configuration2:2.7' |
| 121 | 121 | implementation 'commons-beanutils:commons-beanutils:1.9.4' |
| 122 | 122 | |
| 123 | // Spelling, TeX, Docking | |
| 123 | // Spelling, TeX, Docking, KeenQuotes | |
| 124 | 124 | implementation fileTree(include: ['**/*.jar'], dir: 'libs') |
| 125 | 125 | |
| 80 | 80 | Install and configure the default theme pack as follows: |
| 81 | 81 | |
| 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. | |
| 83 | 83 | 1. Extract archive into a known location. |
| 84 | 84 | 1. Start the text editor, if not already running. |
| 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 | } | |
| 1 | 87 |
| 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 | } | |
| 1 | 207 |
| 4 | 4 | import org.jsoup.nodes.Document; |
| 5 | 5 | |
| 6 | import static com.keenwrite.util.MurmurHash.hash32; | |
| 7 | import static java.lang.System.currentTimeMillis; | |
| 8 | ||
| 9 | 6 | /** |
| 10 | 7 | * Collates information about an HTML document that has changed. |
| 11 | 8 | */ |
| 12 | 9 | public class DocumentChangedEvent implements AppEvent { |
| 13 | private static final int SEED = (int) currentTimeMillis(); | |
| 14 | ||
| 15 | 10 | private final String mText; |
| 16 | 11 | |
| 17 | 12 | /** |
| 18 | * Hash the document so subscribers are only informed upon changes. | |
| 13 | * Hash document (as plain text) so subscribers are notified upon changes. | |
| 19 | 14 | */ |
| 20 | 15 | private static int sHash; |
| ... | ||
| 40 | 35 | */ |
| 41 | 36 | public static void fireDocumentChangedEvent( final Document html ) { |
| 37 | // Hashing the document text ignores caret position changes. | |
| 42 | 38 | final var text = html.wholeText(); |
| 43 | final var hash = hash32( text, 0, text.length(), SEED ); | |
| 39 | final var hash = text.hashCode(); | |
| 44 | 40 | |
| 45 | 41 | if( hash != sHash ) { |
| 176 | 176 | Setting.of( title( KEY_TYPESET_CONTEXT_CLEAN ), |
| 177 | 177 | 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 ) ) | |
| 178 | 184 | ) |
| 179 | 185 | ), |
| 118 | 118 | entry( KEY_LANGUAGE_LOCALE, asLocaleProperty( LOCALE_DEFAULT ) ), |
| 119 | 119 | |
| 120 | entry( KEY_TYPESET_CONTEXT_CLEAN, asBooleanProperty(true) ), | |
| 120 | entry( KEY_TYPESET_CONTEXT_CLEAN, asBooleanProperty( true ) ), | |
| 121 | 121 | 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 ) ) | |
| 123 | 124 | //@formatter:on |
| 124 | 125 | ); |
| 88 | 88 | public static final Key KEY_TYPESET_CONTEXT_THEME_SELECTION = key( KEY_TYPESET_CONTEXT_THEMES, "selection" ); |
| 89 | 89 | 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" ); | |
| 90 | 92 | //@formatter:on |
| 91 | 93 |
| 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 | } | |
| 106 | 1 |
| 2 | 2 | package com.keenwrite.preview; |
| 3 | 3 | |
| 4 | import com.keenwrite.dom.DocumentConverter; | |
| 4 | 5 | import com.keenwrite.ui.adapters.DocumentAdapter; |
| 5 | 6 | import javafx.beans.property.BooleanProperty; |
| ... | ||
| 84 | 85 | } |
| 85 | 86 | |
| 86 | private static final DomConverter CONVERTER = new DomConverter(); | |
| 87 | private static final DocumentConverter CONVERTER = new DocumentConverter(); | |
| 87 | 88 | private static final XhtmlNamespaceHandler XNH = new XhtmlNamespaceHandler(); |
| 88 | 89 | |
| 47 | 47 | */ |
| 48 | 48 | private static final String HTML_STYLESHEET = |
| 49 | "<link rel='stylesheet' href='%s'>"; | |
| 49 | "<link rel='stylesheet' href='%s'/>"; | |
| 50 | 50 | |
| 51 | 51 | private static final String HTML_BASE = |
| 52 | "<base href='%s'>"; | |
| 52 | "<base href='%s'/>"; | |
| 53 | 53 | |
| 54 | 54 | /** |
| ... | ||
| 69 | 69 | """ |
| 70 | 70 | <!doctype html> |
| 71 | <html lang='%s'><head><title> </title><meta charset='utf-8'> | |
| 71 | <html lang='%s'><head><title> </title><meta charset='utf-8'/> | |
| 72 | 72 | %s%s<style>body{font-family:'%s';font-size: %dpx;}</style>%s</head><body> |
| 73 | 73 | """; |
| 14 | 14 | import org.w3c.dom.Element; |
| 15 | 15 | |
| 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; | |
| 20 | 16 | import java.awt.*; |
| 21 | 17 | 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; | |
| 23 | 22 | import java.net.URI; |
| 24 | 23 | import java.nio.file.Path; |
| 25 | 24 | import java.text.NumberFormat; |
| 26 | 25 | import java.text.ParseException; |
| 27 | 26 | |
| 27 | import static com.keenwrite.dom.DocumentParser.transform; | |
| 28 | 28 | import static com.keenwrite.events.StatusEvent.clue; |
| 29 | 29 | import static com.keenwrite.preview.HighQualityRenderingHints.RENDERING_HINTS; |
| 30 | 30 | import static java.awt.image.BufferedImage.TYPE_INT_RGB; |
| 31 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 32 | 31 | import static java.text.NumberFormat.getIntegerInstance; |
| 33 | import static javax.xml.transform.OutputKeys.*; | |
| 34 | 32 | import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH; |
| 35 | 33 | import static org.apache.batik.transcoder.image.ImageTranscoder.KEY_PIXEL_UNIT_TO_MILLIMETER; |
| ... | ||
| 60 | 58 | private static final SAXSVGDocumentFactory FACTORY_DOM = |
| 61 | 59 | 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 | } | |
| 83 | 60 | |
| 84 | 61 | private static final NumberFormat INT_FORMAT = getIntegerInstance(); |
| ... | ||
| 315 | 292 | * element to a string. |
| 316 | 293 | * |
| 317 | * @param e The DOM node to convert to a string. | |
| 294 | * @param root The DOM node to convert to a string. | |
| 318 | 295 | * @return The DOM node as an escaped, plain text string. |
| 319 | 296 | */ |
| 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=\"\" ", "" ); | |
| 324 | 300 | } catch( final Exception ex ) { |
| 325 | 301 | clue( ex ); |
| 12 | 12 | */ |
| 13 | 13 | public final class HtmlPreviewProcessor extends ExecutorProcessor<String> { |
| 14 | ||
| 15 | 14 | /** |
| 16 | 15 | * There is only one preview panel. |
| 34 | 34 | // HTML preview pane. |
| 35 | 35 | // |
| 36 | // Otherwise, bolt on a processor that--after the interpolation and | |
| 36 | // Otherwise, bolt on a processor that---after the interpolation and | |
| 37 | 37 | // substitution phase, which includes text strings or R code---will |
| 38 | 38 | // generate HTML or plain Markdown. HTML has a few output formats: |
| 39 | 39 | // with embedded SVG representing formulas, or without any conversion |
| 40 | 40 | // to SVG. Without conversion would require client-side rendering of |
| 41 | 41 | // math (such as using the JavaScript-based KaTeX engine). |
| 42 | 42 | final var successor = context.isExportFormat( NONE ) |
| 43 | ? createHtmlPreviewProcessor() | |
| 43 | ? createHtmlPreviewProcessor( context ) | |
| 44 | 44 | : context.isExportFormat( XHTML_TEX ) |
| 45 | 45 | ? createXhtmlProcessor( context ) |
| 46 | 46 | : context.isExportFormat( APPLICATION_PDF ) |
| 47 | 47 | ? createPdfProcessor( context ) |
| 48 | : createIdentityProcessor(); | |
| 48 | : createIdentityProcessor( context ); | |
| 49 | 49 | |
| 50 | 50 | final var processor = switch( context.getFileType() ) { |
| ... | ||
| 74 | 74 | * @return An instance of {@link Processor} that performs no processing. |
| 75 | 75 | */ |
| 76 | private Processor<String> createIdentityProcessor() { | |
| 76 | @SuppressWarnings( "unused" ) | |
| 77 | private Processor<String> createIdentityProcessor( | |
| 78 | final ProcessorContext ignored ) { | |
| 77 | 79 | return IDENTITY; |
| 78 | 80 | } |
| 79 | 81 | |
| 80 | 82 | /** |
| 81 | 83 | * Instantiates a new {@link Processor} that passes an incoming HTML |
| 82 | 84 | * string to a user interface widget that can render HTML as a web page. |
| 83 | 85 | * |
| 84 | 86 | * @return An instance of {@link Processor} that forwards HTML for display. |
| 85 | 87 | */ |
| 86 | private Processor<String> createHtmlPreviewProcessor() { | |
| 88 | @SuppressWarnings( "unused" ) | |
| 89 | private Processor<String> createHtmlPreviewProcessor( | |
| 90 | final ProcessorContext ignored ) { | |
| 87 | 91 | return new HtmlPreviewProcessor( getPreviewPane() ); |
| 88 | 92 | } |
| 2 | 2 | package com.keenwrite.processors; |
| 3 | 3 | |
| 4 | import com.keenwrite.dom.DocumentParser; | |
| 4 | 5 | import com.keenwrite.preferences.Key; |
| 5 | 6 | import com.keenwrite.preferences.Workspace; |
| 6 | 7 | import com.keenwrite.ui.heuristics.WordCounter; |
| 8 | import com.whitemagicsoftware.keenquotes.Converter; | |
| 7 | 9 | import javafx.beans.property.StringProperty; |
| 8 | import org.jsoup.nodes.Document; | |
| 10 | import org.w3c.dom.Document; | |
| 9 | 11 | |
| 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; | |
| 14 | 12 | import java.io.FileNotFoundException; |
| 15 | 13 | import java.nio.file.Path; |
| 16 | 14 | import java.util.Locale; |
| 17 | 15 | import java.util.Map; |
| 18 | import java.util.Map.Entry; | |
| 19 | 16 | import java.util.regex.Pattern; |
| 20 | 17 | |
| 21 | 18 | import static com.keenwrite.Bootstrap.APP_TITLE_LOWERCASE; |
| 19 | import static com.keenwrite.dom.DocumentParser.*; | |
| 22 | 20 | import static com.keenwrite.events.StatusEvent.clue; |
| 23 | 21 | import static com.keenwrite.io.HttpFacade.httpGet; |
| 24 | 22 | import static com.keenwrite.preferences.WorkspaceKeys.*; |
| 25 | 23 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; |
| 26 | 24 | 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; | |
| 27 | 27 | import static java.lang.String.format; |
| 28 | 28 | import static java.lang.String.valueOf; |
| 29 | 29 | import static java.nio.file.Files.copy; |
| 30 | 30 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; |
| 31 | 31 | import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS; |
| 32 | 32 | 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; | |
| 37 | 33 | |
| 38 | 34 | /** |
| 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 | |
| 40 | 36 | * and body elements. This doesn't have to be super-efficient because it's |
| 41 | 37 | * not run in real-time. |
| 42 | 38 | */ |
| 43 | 39 | public final class XhtmlProcessor extends ExecutorProcessor<String> { |
| 44 | 40 | private final static Pattern BLANK = |
| 45 | 41 | compile( "\\p{Blank}", UNICODE_CHARACTER_CLASS ); |
| 42 | ||
| 43 | private final static Converter sTypographer = | |
| 44 | new Converter( lex -> clue( lex.toString() ), CHARS, PARSER_XML ); | |
| 46 | 45 | |
| 47 | 46 | private final ProcessorContext mContext; |
| ... | ||
| 66 | 65 | clue( "Main.status.typeset.xhtml" ); |
| 67 | 66 | |
| 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 ); | |
| 71 | 70 | |
| 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(); | |
| 75 | 74 | |
| 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 ); | |
| 80 | 94 | } |
| 81 | 95 | |
| 82 | return doc.html(); | |
| 96 | return html; | |
| 83 | 97 | } |
| 84 | 98 | |
| 85 | 99 | /** |
| 86 | 100 | * Applies the metadata fields to the document. |
| 87 | 101 | * |
| 88 | 102 | * @param doc The document to adorn with metadata. |
| 89 | 103 | */ |
| 90 | 104 | private void setMetaData( final Document doc ) { |
| 91 | doc.title( getTitle() ); | |
| 92 | ||
| 93 | 105 | final var metadata = createMetaData( doc ); |
| 94 | final var head = doc.head(); | |
| 95 | metadata.entrySet().forEach( entry -> head.append( createMeta( entry ) ) ); | |
| 96 | } | |
| 97 | 106 | |
| 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 ) ) ) | |
| 101 | 110 | ); |
| 111 | walk( doc, "/html/head/title", node -> node.setTextContent( title() ) ); | |
| 102 | 112 | } |
| 103 | 113 | |
| ... | ||
| 110 | 120 | private Map<String, String> createMetaData( final Document doc ) { |
| 111 | 121 | 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() | |
| 121 | 131 | ); |
| 122 | 132 | } |
| ... | ||
| 149 | 159 | // Strip comments, superfluous whitespace, DOCTYPE, and XML declarations. |
| 150 | 160 | if( mediaType.isSvg() ) { |
| 151 | sanitize( imageFile ); | |
| 161 | DocumentParser.sanitize( imageFile ); | |
| 152 | 162 | } |
| 153 | 163 | } |
| ... | ||
| 183 | 193 | |
| 184 | 194 | 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 ); | |
| 213 | 195 | } |
| 214 | 196 | |
| ... | ||
| 235 | 217 | } |
| 236 | 218 | |
| 237 | private Locale getLocale() { return getWorkspace().getLocale(); } | |
| 219 | private Locale locale() { return getWorkspace().getLocale(); } | |
| 238 | 220 | |
| 239 | private String getTitle() { | |
| 221 | private String title() { | |
| 240 | 222 | return resolve( KEY_DOC_TITLE ); |
| 241 | 223 | } |
| 242 | 224 | |
| 243 | private String getAuthor() { | |
| 225 | private String author() { | |
| 244 | 226 | return resolve( KEY_DOC_AUTHOR ); |
| 245 | 227 | } |
| 246 | 228 | |
| 247 | private String getByline() { | |
| 229 | private String byLine() { | |
| 248 | 230 | return resolve( KEY_DOC_BYLINE ); |
| 249 | 231 | } |
| 250 | 232 | |
| 251 | private String getAddress() { | |
| 233 | private String address() { | |
| 252 | 234 | return resolve( KEY_DOC_ADDRESS ).replaceAll( "\n", "\\\\\\break{}" ); |
| 253 | 235 | } |
| 254 | 236 | |
| 255 | private String getPhone() { | |
| 237 | private String phone() { | |
| 256 | 238 | return resolve( KEY_DOC_PHONE ); |
| 257 | 239 | } |
| 258 | 240 | |
| 259 | private String getEmail() { | |
| 241 | private String email() { | |
| 260 | 242 | return resolve( KEY_DOC_EMAIL ); |
| 261 | 243 | } |
| 262 | 244 | |
| 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() ) ); | |
| 267 | 255 | } |
| 268 | 256 | |
| 269 | private String getKeywords() { | |
| 257 | private String keywords() { | |
| 270 | 258 | return resolve( KEY_DOC_KEYWORDS ); |
| 271 | 259 | } |
| 272 | 260 | |
| 273 | private String getCopyright() { | |
| 261 | private String copyright() { | |
| 274 | 262 | return resolve( KEY_DOC_COPYRIGHT ); |
| 275 | 263 | } |
| 276 | 264 | |
| 277 | private String getDate() { | |
| 265 | private String date() { | |
| 278 | 266 | 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 ); | |
| 279 | 276 | } |
| 280 | 277 | |
| 11 | 11 | import com.vladsch.flexmark.ext.superscript.SuperscriptExtension; |
| 12 | 12 | import com.vladsch.flexmark.ext.tables.TablesExtension; |
| 13 | import com.vladsch.flexmark.ext.typographic.TypographicExtension; | |
| 14 | 13 | import com.vladsch.flexmark.html.HtmlRenderer; |
| 15 | 14 | import com.vladsch.flexmark.parser.Parser; |
| 16 | 15 | import com.vladsch.flexmark.util.ast.IParse; |
| 17 | 16 | import com.vladsch.flexmark.util.ast.IRender; |
| 18 | 17 | import com.vladsch.flexmark.util.ast.Node; |
| 19 | 18 | import com.vladsch.flexmark.util.misc.Extension; |
| 20 | 19 | |
| 21 | 20 | import java.util.ArrayList; |
| 22 | 21 | import java.util.List; |
| 23 | ||
| 24 | import static com.keenwrite.ExportFormat.APPLICATION_PDF; | |
| 25 | import static com.vladsch.flexmark.ext.typographic.TypographicExtension.ENABLE_SMARTS; | |
| 26 | 22 | |
| 27 | 23 | /** |
| ... | ||
| 39 | 35 | super( successor ); |
| 40 | 36 | |
| 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. | |
| 44 | 37 | final var builder = Parser.builder(); |
| 45 | builder.set( ENABLE_SMARTS, !context.isExportFormat( APPLICATION_PDF ) ); | |
| 46 | ||
| 47 | 38 | final var extensions = createExtensions( context ); |
| 48 | 39 | mParser = builder.extensions( extensions ).build(); |
| 49 | 40 | mRenderer = HtmlRenderer.builder().extensions( extensions ).build(); |
| 50 | 41 | } |
| 51 | 42 | |
| 52 | 43 | /** |
| 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. | |
| 56 | 45 | * |
| 57 | 46 | * @param context The context that subclasses use to configure custom |
| ... | ||
| 68 | 57 | extensions.add( TablesExtension.create() ); |
| 69 | 58 | extensions.add( FencedDivExtension.create() ); |
| 70 | ||
| 71 | if( !context.isExportFormat( APPLICATION_PDF ) ) { | |
| 72 | extensions.add( TypographicExtension.create() ); | |
| 73 | } | |
| 74 | 59 | |
| 75 | 60 | return extensions; |
| 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 = "‘"; | |
| 57 | ||
| 58 | /** | |
| 59 | * Right single quote replacement text. | |
| 60 | */ | |
| 61 | private static final String QUOTE_SINGLE_RIGHT = "’"; | |
| 62 | ||
| 63 | /** | |
| 64 | * Left double quote replacement text. | |
| 65 | */ | |
| 66 | private static final String QUOTE_DOUBLE_LEFT = "“"; | |
| 67 | ||
| 68 | /** | |
| 69 | * Right double quote replacement text. | |
| 70 | */ | |
| 71 | private static final String QUOTE_DOUBLE_RIGHT = "”"; | |
| 72 | ||
| 73 | /** | |
| 74 | * Apostrophe replacement text. | |
| 75 | */ | |
| 76 | private static final String APOSTROPHE = "'"; | |
| 77 | ||
| 78 | /** | |
| 79 | * Prime replacement text. | |
| 80 | */ | |
| 81 | private static final String SINGLE_PRIME = "′"; | |
| 82 | ||
| 83 | /** | |
| 84 | * Double prime replacement text. | |
| 85 | */ | |
| 86 | private static final String DOUBLE_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 | } | |
| 320 | 1 |
| 14 | 14 | import java.io.FileInputStream; |
| 15 | 15 | import java.io.IOException; |
| 16 | import java.io.InputStreamReader; | |
| 17 | import java.nio.charset.StandardCharsets; | |
| 16 | 18 | import java.nio.file.Path; |
| 17 | 19 | import java.util.Properties; |
| ... | ||
| 25 | 27 | import static com.keenwrite.util.FileWalker.walk; |
| 26 | 28 | import static java.lang.Math.max; |
| 29 | import static java.nio.charset.StandardCharsets.UTF_8; | |
| 27 | 30 | import static org.codehaus.plexus.util.StringUtils.abbreviate; |
| 28 | 31 | |
| ... | ||
| 148 | 151 | } |
| 149 | 152 | |
| 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 { | |
| 151 | 162 | final var properties = new Properties(); |
| 152 | 163 | |
| 153 | try( final var in = new FileInputStream( file.toFile() ) ) { | |
| 164 | try( final var in = new InputStreamReader( | |
| 165 | new FileInputStream( path.toFile() ), UTF_8 ) ) { | |
| 154 | 166 | properties.load( in ); |
| 155 | 167 | } |
| 5 | 5 | import com.keenwrite.preferences.Workspace; |
| 6 | 6 | import com.keenwrite.preview.HtmlPanel; |
| 7 | import com.keenwrite.util.MurmurHash; | |
| 8 | 7 | import com.whitemagicsoftware.wordcount.TokenizerException; |
| 9 | 8 | import javafx.beans.property.IntegerProperty; |
| ... | ||
| 16 | 15 | import javafx.scene.control.TableView; |
| 17 | 16 | import org.greenrobot.eventbus.Subscribe; |
| 18 | import org.jsoup.Jsoup; | |
| 19 | 17 | |
| 20 | 18 | import static com.keenwrite.events.Bus.register; |
| ... | ||
| 68 | 66 | |
| 69 | 67 | /** |
| 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 | |
| 71 | 69 | * 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. | |
| 78 | 75 | * |
| 79 | 76 | * @param event Container for the document text that has changed. |
| 80 | 77 | */ |
| 81 | 78 | @Subscribe |
| 82 | 79 | public void handle( final DocumentChangedEvent event ) { |
| 83 | 80 | try { |
| 84 | 81 | runLater( () -> { |
| 85 | 82 | mItems.clear(); |
| 86 | 83 | final var document = event.getDocument(); |
| 87 | final var wordCount = mWordCounter.countWords( | |
| 84 | final var wordCount = mWordCounter.count( | |
| 88 | 85 | document, ( k, count ) -> { |
| 89 | 86 | // Generate statistics for words that occur thrice or more. |
| 33 | 33 | * @return The total number of words in the document. |
| 34 | 34 | */ |
| 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 ) -> {} ); | |
| 37 | 37 | } |
| 38 | 38 | |
| 39 | 39 | /** |
| 40 | 40 | * Counts the number of unique words in the document. |
| 41 | 41 | * |
| 42 | 42 | * @param document The document to tally. |
| 43 | 43 | * @param consumer The action to take for each unique word/count pair. |
| 44 | 44 | * @return The total number of words in the document. |
| 45 | 45 | */ |
| 46 | public int countWords( | |
| 46 | public int count( | |
| 47 | 47 | final String document, final BiConsumer<String, Integer> consumer ) { |
| 48 | 48 | final var tokens = mTokenizer.tokenize( document ); |
| 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 | } | |
| 127 | 1 |
| 46 | 46 | workspace.typeset.context.clean.desc=Delete ancillary files after an unsuccessful export. |
| 47 | 47 | 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 | |
| 48 | 52 | |
| 49 | 53 | workspace.r=R |
| 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 | } | |
| 1 | 60 |
| 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 | } | |
| 60 | 1 |
| 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 | } | |
| 164 | 1 |
| 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 | } | |
| 1 | 152 |
| 1 | # ######################################################################## | |
| 2 | # Decades | |
| 3 | # ######################################################################## | |
| 4 | The Roaring '20s had the best music, no? | |
| 5 | The Roaring '20s had the best music, no? | |
| 6 | ||
| 7 | Took place in '04, yes'm! | |
| 8 | Took place in '04, yes'm! | |
| 9 | ||
| 10 | # ######################################################################## | |
| 11 | # Inside contractions (no leading/trailing apostrophes) | |
| 12 | # ######################################################################## | |
| 13 | I don't like it: I love's it! | |
| 14 | I don't like it: I love's it! | |
| 15 | ||
| 16 | We'd've thought that pancakes'll be sweeter there. | |
| 17 | We'd've thought that pancakes'll be sweeter there. | |
| 18 | ||
| 19 | She'd be coming o'er when the horse'd gone to pasture... | |
| 20 | She'd be coming o'er when the horse'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 | 'Twas and 'tis whate'er lay 'twixt dawn and dusk 'n River Styx. | |
| 27 | ||
| 28 | # ######################################################################## | |
| 29 | # Ending contractions (trailing apostrophes) | |
| 30 | # ######################################################################## | |
| 31 | Didn' get th' message. | |
| 32 | Didn' get th' message. | |
| 33 | ||
| 34 | Namsayin', y'know what I'ma sayin'? | |
| 35 | Namsayin', y'know what I'ma sayin'? | |
| 36 | ||
| 37 | # ######################################################################## | |
| 38 | # Outside contractions (leading and trailing, no middle) | |
| 39 | # ######################################################################## | |
| 40 | Salt 'n' vinegar, fish-'n'-chips, sugar 'n' spice! | |
| 41 | Salt 'n' vinegar, fish-'n'-chips, sugar 'n' spice! | |
| 42 | ||
| 43 | # ######################################################################## | |
| 44 | # Primes (single, double) | |
| 45 | # ######################################################################## | |
| 46 | She stood 5\'7\". | |
| 47 | She stood 5′7″. | |
| 48 | ||
| 49 | # No space after the feet sign. | |
| 50 | It's 4'11" away. | |
| 51 | It's 4′11″ away. | |
| 52 | ||
| 53 | Alice's friend is 6'3" tall. | |
| 54 | Alice's friend is 6′3″ tall. | |
| 55 | ||
| 56 | Bob's table is 5'' × 4''. | |
| 57 | Bob's table is 5″ × 4″. | |
| 58 | ||
| 59 | What's this -5.5'' all about? | |
| 60 | What's this -5.5″ all about? | |
| 61 | ||
| 62 | +7.9'' is weird. | |
| 63 | +7.9″ is weird. | |
| 64 | ||
| 65 | Foolscap? Naw, I use 11.5"x14.25" paper! | |
| 66 | Foolscap? Naw, I use 11.5″x14.25″ paper! | |
| 67 | ||
| 68 | An angular measurement, 3° 5' 30" means 3 degs, 5 arcmins, and 30 arcsecs. | |
| 69 | An angular measurement, 3° 5′ 30″ means 3 degs, 5 arcmins, and 30 arcsecs. | |
| 70 | ||
| 71 | # ######################################################################## | |
| 72 | # Backticks (left and right double quotes) | |
| 73 | # ######################################################################## | |
| 74 | ``I am Sam'' | |
| 75 | “I am Sam” | |
| 76 | ||
| 77 | ``Sam's away today'' | |
| 78 | “Sam's away today” | |
| 79 | ||
| 80 | ``Sam's gone! | |
| 81 | “Sam's gone! | |
| 82 | ||
| 83 | ``5'10" tall 'e was!'' | |
| 84 | “5′10″ tall 'e was!” | |
| 85 | ||
| 86 | # ######################################################################## | |
| 87 | # Consecutive quotes | |
| 88 | # ######################################################################## | |
| 89 | "'I'm trouble.'" | |
| 90 | “‘I'm trouble.’” | |
| 91 | ||
| 92 | '"Trouble's my name."' | |
| 93 | ‘“Trouble's my name.“‘ | |
| 94 | ||
| 95 | # ######################################################################## | |
| 96 | # Escaped quotes | |
| 97 | # ######################################################################## | |
| 98 | \"What?\" | |
| 99 | “What?” | |
| 100 | ||
| 101 | # ######################################################################## | |
| 102 | # Double quotes | |
| 103 | # ######################################################################## | |
| 104 | "I am Sam" | |
| 105 | “I am Sam” | |
| 106 | ||
| 107 | "...even better!" | |
| 108 | “...even better!” | |
| 109 | ||
| 110 | "It was so," said he. | |
| 111 | “It was so,” said he. | |
| 112 | ||
| 113 | "She said, 'Llamas'll languish, they'll-- | |
| 114 | “She said, ‘Llamas'll languish, they'll-- | |
| 115 | ||
| 116 | With "air quotes" in the middle. | |
| 117 | With “air quotes” in the middle. | |
| 118 | ||
| 119 | With--"air quotes"--and dashes. | |
| 120 | With--“air quotes”--and dashes. | |
| 121 | ||
| 122 | "Not "quite" what you expected?" | |
| 123 | “Not “quite” what you expected?” | |
| 124 | ||
| 125 | # ######################################################################## | |
| 126 | # Nested quotations | |
| 127 | # ######################################################################## | |
| 128 | "'Here I am,' said Sam" | |
| 129 | “‘Here I am,’ said Sam” | |
| 130 | ||
| 131 | '"Here I am," said Sam' | |
| 132 | ‘“Here I am,”, said Sam’ | |
| 133 | ||
| 134 | 'Hello, "Dr. Brown," what's your real name?' | |
| 135 | ‘Hello, “Dr. Brown,” what's your real name?’ | |
| 136 | ||
| 137 | "'Twas, t'wasn't thy name, 'twas it?" said Jim "the Barber" Brown. | |
| 138 | “'Twas, t'wasn't thy name, 'twas it?” said Jim “the Barber” Brown. | |
| 139 | ||
| 140 | # ######################################################################## | |
| 141 | # Single quotes | |
| 142 | # ######################################################################## | |
| 143 | 'I am Sam' | |
| 144 | ‘I am Sam’ | |
| 145 | ||
| 146 | 'It was so,' said he. | |
| 147 | ‘It was so,’ said he. | |
| 148 | ||
| 149 | '...even better!' | |
| 150 | ‘...even better!’ | |
| 151 | ||
| 152 | With 'quotes' in the middle. | |
| 153 | With ‘quotes’ in the middle. | |
| 154 | ||
| 155 | With--'imaginary'--dashes. | |
| 156 | With--‘imaginary’--dashes. | |
| 157 | ||
| 158 | 'Not 'quite' what you expected?' | |
| 159 | ‘Not ‘quite’ what you expected?’ | |
| 160 | ||
| 161 | ''Cause I don't like it, 's why,' said Pat. | |
| 162 | ‘'Cause I don't like it, 's why,’ said Pat. | |
| 163 | ||
| 164 | 'It's a beautiful day!' | |
| 165 | ‘It's a beautiful day!’ | |
| 166 | ||
| 167 | 'He said, 'Thinkin'.' | |
| 168 | ‘He said, ‘Thinkin’.’ | |
| 169 | ||
| 170 | # ######################################################################## | |
| 171 | # Possessives | |
| 172 | # ######################################################################## | |
| 173 | Sam's Sams' and the Ross's roses' thorns were prickly. | |
| 174 | Sam's Sams' and the Ross's roses' thorns were prickly. | |
| 175 | ||
| 176 | # ######################################################################## | |
| 177 | # Mixed | |
| 178 | # ######################################################################## | |
| 179 | "I heard she said, 'That's Sam's'," said the Sams' cat. | |
| 180 | “I heard she said, ‘That's Sam's’,” said the Sams' 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 | “‘Janes' said, ‘'E'll be spooky, Sam's son with the jack-o'-lantern!’” said the O'Mally twins'---y'know---ghosts in unison. | |
| 184 | ||
| 185 | 'He's at Sams' | |
| 186 | ‘He' at Sams’ | |
| 187 | ||
| 188 | \"Hello!\" | |
| 189 | “Hello!” | |
| 190 | ||
| 191 | ma'am | |
| 192 | ma'am | |
| 193 | ||
| 194 | 'Twas midnight | |
| 195 | 'Twas midnight | |
| 196 | ||
| 197 | \"Hello,\" said the spider. \"'Shelob' is my name.\" | |
| 198 | “Hello,” said the spider. “‘Shelob’ is my name.” | |
| 199 | ||
| 200 | 'A', 'B', and 'C' are letters. | |
| 201 | ‘A’ ‘B’ and ‘C’ are letters. | |
| 202 | ||
| 203 | 'Oak,' 'elm,' and 'beech' are names of trees. So is 'pine.' | |
| 204 | ‘Oak,’ ‘elm,’ and ‘beech’ are names of trees. So is ‘pine.’ | |
| 205 | ||
| 206 | 'He said, \"I want to go.\"' Were you alive in the 70's? | |
| 207 | ‘He said, “I want to go.”’ Were you alive in the 70's? | |
| 208 | ||
| 209 | \"That's a 'magic' sock.\" | |
| 210 | “That's a ‘magic’ sock.” | |
| 211 | ||
| 212 | Website! Company Name, Inc. (\"Company Name\" or \"Company\") recommends reading the following terms and conditions, carefully: | |
| 213 | Website! Company Name, Inc. (“Company Name” or “Company”) 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. (‘Company Name’ or ‘Company’) recommends reading the following terms and conditions, carefully: | |
| 217 | ||
| 218 | Workin' hard | |
| 219 | Workin' hard | |
| 220 | ||
| 221 | '70s are my favorite numbers,' she said. | |
| 222 | ‘70s are my favorite numbers,’ she said. | |
| 223 | ||
| 224 | '70s fashion was weird. | |
| 225 | '70s fashion was weird. | |
| 226 | ||
| 227 | 12\" record, 5'10\" height | |
| 228 | 12″ record, 5′10″ height | |
| 229 | ||
| 230 | Model \"T2000\" | |
| 231 | Model “T2000” | |
| 232 | ||
| 233 | iPad 3's battery life is not great. | |
| 234 | iPad 3's battery life is not great. | |
| 235 | ||
| 236 | Book 'em, Danno. Rock 'n' roll. 'Cause 'twas the season. | |
| 237 | Book 'em, Danno. Rock 'n' roll. 'Cause 'twas the season. | |
| 238 | ||
| 239 | '85 was a good year. (The entire '80s were.) | |
| 240 | '85 was a good year. (The entire '80s were.) | |
| 241 | ||
| 242 | 1 |