| 7 | 7 | Download and install the following software packages: |
| 8 | 8 | |
| 9 | * [JDK 21](https://bell-sw.com/pages/downloads) (Full JDK + JavaFX) | |
| 10 | * [Gradle 8.5](https://gradle.org/releases) | |
| 11 | * [Git 2.43](https://git-scm.com/downloads) | |
| 9 | * [JDK 22](https://bell-sw.com/pages/downloads) (Full JDK + JavaFX) | |
| 10 | * [Gradle 8.9](https://gradle.org/releases) | |
| 11 | * [Git 2.45](https://git-scm.com/downloads) | |
| 12 | 12 | * [warp v0.4.0-alpha](https://github.com/Reisz/warp/releases/tag/v0.4.0) |
| 13 | 13 |
| 9 | 9 | } |
| 10 | 10 | dependencies { |
| 11 | classpath 'org.owasp:dependency-check-gradle:9.0.1' | |
| 12 | classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:5.2.4" | |
| 11 | classpath 'org.owasp:dependency-check-gradle:10.0.3' | |
| 12 | classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.19" | |
| 13 | 13 | } |
| 14 | 14 | } |
| 15 | 15 | |
| 16 | 16 | plugins { |
| 17 | 17 | id 'application' |
| 18 | 18 | id 'org.openjfx.javafxplugin' version '0.1.0' |
| 19 | id 'com.palantir.git-version' version '3.0.0' | |
| 19 | id 'com.palantir.git-version' version '3.1.0' | |
| 20 | 20 | id 'com.github.spotbugs' version '6.0.9' |
| 21 | 21 | } |
| ... | ||
| 42 | 42 | content { |
| 43 | 43 | includeGroup 'com.github.css4j' |
| 44 | includeGroup 'io.sf.graphics' | |
| 44 | 45 | includeGroup 'io.sf.carte' |
| 45 | 46 | includeGroup 'io.sf.jclf' |
| ... | ||
| 78 | 79 | |
| 79 | 80 | java { |
| 80 | sourceCompatibility = 21 | |
| 81 | targetCompatibility = 21 | |
| 81 | sourceCompatibility = 22 | |
| 82 | targetCompatibility = 22 | |
| 82 | 83 | } |
| 83 | 84 | |
| 84 | 85 | javafx { |
| 85 | 86 | version = '21' |
| 86 | 87 | modules = ['javafx.base', 'javafx.controls', 'javafx.graphics', 'javafx.swing'] |
| 87 | 88 | configuration = 'compileOnly' |
| 88 | 89 | } |
| 89 | 90 | |
| 90 | 91 | dependencies { |
| 91 | def v_junit = '5.10.2' | |
| 92 | def v_junit = '5.10.3' | |
| 92 | 93 | def v_flexmark = '0.64.8' |
| 93 | def v_jackson = '2.17.0' | |
| 94 | def v_echosvg = '1.0.1' | |
| 95 | def v_picocli = '4.7.5' | |
| 94 | def v_jackson = '2.17.2' | |
| 95 | def v_echosvg = '1.2' | |
| 96 | def v_picocli = '4.7.6' | |
| 96 | 97 | |
| 97 | 98 | // JavaFX |
| 98 | implementation 'org.controlsfx:controlsfx:11.2.0' | |
| 99 | implementation 'org.fxmisc.richtext:richtextfx:0.11.2' | |
| 100 | implementation 'org.fxmisc.flowless:flowless:0.7.2' | |
| 99 | implementation 'org.controlsfx:controlsfx:11.2.1' | |
| 100 | implementation 'org.fxmisc.richtext:richtextfx:0.11.3' | |
| 101 | implementation 'org.fxmisc.flowless:flowless:0.7.3' | |
| 101 | 102 | implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3' |
| 102 | 103 | implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.17.0' |
| 103 | implementation 'com.panemu:tiwulfx-dock:0.2' | |
| 104 | implementation 'com.panemu:tiwulfx-dock:0.3' | |
| 104 | 105 | |
| 105 | 106 | // Markdown |
| ... | ||
| 119 | 120 | |
| 120 | 121 | // HTML parsing and rendering |
| 121 | implementation 'org.jsoup:jsoup:1.17.1' | |
| 122 | implementation 'org.xhtmlrenderer:flying-saucer-core:9.7.1' | |
| 122 | implementation 'org.jsoup:jsoup:1.18.1' | |
| 123 | implementation 'org.xhtmlrenderer:flying-saucer-core:9.9.0' | |
| 123 | 124 | |
| 124 | 125 | // R |
| 125 | 126 | implementation 'org.apache.commons:commons-compress:1.26.1' |
| 126 | implementation 'org.codehaus.plexus:plexus-utils:4.0.0' | |
| 127 | implementation 'org.codehaus.plexus:plexus-utils:4.0.1' | |
| 127 | 128 | implementation 'org.renjin:renjin-script-engine:3.5-beta76' |
| 128 | 129 | implementation 'org.renjin.cran:rjson:0.2.15-renjin-21' |
| ... | ||
| 144 | 145 | // Misc. |
| 145 | 146 | implementation 'org.ahocorasick:ahocorasick:0.6.3' |
| 146 | implementation 'com.github.albfernandez:juniversalchardet:2.4.0' | |
| 147 | implementation 'jakarta.validation:jakarta.validation-api:3.0.2' | |
| 147 | implementation 'com.github.albfernandez:juniversalchardet:2.5.0' | |
| 148 | implementation 'jakarta.validation:jakarta.validation-api:3.1.0' | |
| 148 | 149 | implementation 'org.greenrobot:eventbus-java:3.3.1' |
| 149 | 150 | |
| ... | ||
| 166 | 167 | testImplementation "org.junit.jupiter:junit-jupiter-params:${v_junit}" |
| 167 | 168 | testImplementation 'org.testfx:testfx-junit5:4.0.18' |
| 168 | testImplementation 'org.assertj:assertj-core:3.25.3' | |
| 169 | testImplementation 'org.assertj:assertj-core:3.26.3' | |
| 169 | 170 | testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' |
| 170 | 171 | } |
| 27 | 27 | |
| 28 | 28 | ``` |
| 29 |  | |
| 29 |  | |
| 30 | 30 | |
| 31 | 31 | :: Figure caption text |
| ... | ||
| 75 | 75 | |
| 76 | 76 | ``` |
| 77 | In [@fig:kitten], a cute kitten is shown. | |
| 77 | In [@fig:animal], a cute animal is shown. | |
| 78 | 78 | |
| 79 |  | |
| 79 |  | |
| 80 | 80 | |
| 81 | :: World's cutest kitten {#fig:kitten} | |
| 81 | :: World's cutest animal {#fig:animal} | |
| 82 | 82 | |
| 83 | There is no cuter kitten than the one in [@fig:kitten]. | |
| 83 | There is no cuter animal than the one in [@fig:animal]. | |
| 84 | 84 | ``` |
| 85 | 85 | |
| ... | ||
| 103 | 103 | generated by the typesetting system: |
| 104 | 104 | |
| 105 | | Type name | English name | |
| 106 | |---|---| | |
| 107 | | algorithm | Algorithm | | |
| 108 | | alg | Algorithm | | |
| 109 | | equation | Equation | | |
| 110 | | eqn | Equation | | |
| 111 | | eq | Equation | | |
| 112 | | figure | Figure | | |
| 113 | | fig | Figure | | |
| 114 | | formula | Formula | | |
| 115 | | listing | Listing | | |
| 116 | | list | Listing | | |
| 117 | | lst | Listing | | |
| 118 | | lyric | Lyrics | | |
| 119 | | music | Score | | |
| 120 | | score | Score | | |
| 121 | | source | Listing | | |
| 122 | | src | Listing | | |
| 123 | | tab | Table | | |
| 124 | | table | Table | | |
| 125 | | tbl | Table | | |
| 105 | | Type name | English name | | |
| 106 | |-----------|--------------| | |
| 107 | | algorithm | Algorithm | | |
| 108 | | alg | Algorithm | | |
| 109 | | equation | Equation | | |
| 110 | | eqn | Equation | | |
| 111 | | eq | Equation | | |
| 112 | | figure | Figure | | |
| 113 | | fig | Figure | | |
| 114 | | formula | Formula | | |
| 115 | | listing | Listing | | |
| 116 | | list | Listing | | |
| 117 | | lst | Listing | | |
| 118 | | lyric | Lyrics | | |
| 119 | | music | Score | | |
| 120 | | score | Score | | |
| 121 | | source | Listing | | |
| 122 | | src | Listing | | |
| 123 | | tab | Table | | |
| 124 | | table | Table | | |
| 125 | | tbl | Table | | |
| 126 | 126 | |
| 127 | 127 | These values are defined in the theme's `xhtml/xml-references.tex` file. |
| 128 | ||
| 129 | 128 | |
| 6 | 6 | |
| 7 | 7 | import com.keenwrite.io.MediaType; |
| 8 | import com.keenwrite.util.EncodingDetector; | |
| 8 | 9 | import javafx.beans.property.ReadOnlyBooleanProperty; |
| 9 | 10 | import javafx.scene.Node; |
| 10 | import org.mozilla.universalchardet.UniversalDetector; | |
| 11 | 11 | |
| 12 | 12 | import java.io.File; |
| 13 | 13 | import java.nio.charset.Charset; |
| 14 | 14 | import java.nio.file.Path; |
| 15 | 15 | |
| 16 | 16 | import static com.keenwrite.constants.Constants.DEFAULT_CHARSET; |
| 17 | 17 | import static com.keenwrite.events.StatusEvent.clue; |
| 18 | 18 | import static com.keenwrite.io.SysFile.toFile; |
| 19 | import static java.nio.charset.Charset.forName; | |
| 20 | 19 | import static java.nio.file.Files.readAllBytes; |
| 21 | 20 | import static java.nio.file.Files.write; |
| 22 | 21 | import static java.util.Arrays.asList; |
| 23 | import static java.util.Locale.ENGLISH; | |
| 24 | 22 | |
| 25 | 23 | /** |
| ... | ||
| 219 | 217 | |
| 220 | 218 | private Charset detectEncoding( final byte[] bytes ) { |
| 221 | final var detector = new UniversalDetector( null ); | |
| 222 | detector.handleData( bytes, 0, bytes.length ); | |
| 223 | detector.dataEnd(); | |
| 224 | ||
| 225 | final var detectedCharset = detector.getDetectedCharset(); | |
| 226 | ||
| 227 | // TODO: Revert when the issue has been fixed. | |
| 228 | // https://github.com/albfernandez/juniversalchardet/issues/35 | |
| 229 | return switch( detectedCharset ) { | |
| 230 | case null -> DEFAULT_CHARSET; | |
| 231 | case "US-ASCII", "TIS620" -> DEFAULT_CHARSET; | |
| 232 | default -> forName( detectedCharset.toUpperCase( ENGLISH ) ); | |
| 233 | }; | |
| 219 | return new EncodingDetector().detect( bytes ); | |
| 234 | 220 | } |
| 235 | 221 | |
| 18 | 18 | |
| 19 | 19 | /** |
| 20 | * Transforms a JsonNode hierarchy into a tree that can be displayed in a user | |
| 21 | * interface and vice-versa. | |
| 20 | * Transforms a {@link JsonNode} hierarchy into a tree that can be displayed | |
| 21 | * in a user interface and vice-versa. | |
| 22 | 22 | */ |
| 23 | 23 | public final class YamlTreeTransformer implements TreeTransformer { |
| ... | ||
| 98 | 98 | // If the current item has more than one non-leaf child, it's an |
| 99 | 99 | // object node and must become a new nested object. |
| 100 | if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) { | |
| 100 | if( !(children.size() == 1 && children.getFirst().isLeaf()) ) { | |
| 101 | 101 | node = node.putObject( item.getValue() ); |
| 102 | 102 | } |
| 44 | 44 | */ |
| 45 | 45 | public static String eval( final String r ) { |
| 46 | return sCache.computeIfAbsent( r, __ -> evaluate( r ) ); | |
| 46 | return sCache.computeIfAbsent( r, _ -> evaluate( r ) ); | |
| 47 | 47 | } |
| 48 | 48 |
| 24 | 24 | |
| 25 | 25 | setEditable( true ); |
| 26 | setCellFactory( treeView -> new AltTreeCell<>( converter ) ); | |
| 26 | setCellFactory( _ -> new AltTreeCell<>( converter ) ); | |
| 27 | 27 | setShowRoot( false ); |
| 28 | 28 | |
| 29 | 29 | // When focus is lost while not editing, deselect all items. |
| 30 | focusedProperty().addListener( ( c, o, n ) -> { | |
| 30 | focusedProperty().addListener( ( _, o, _ ) -> { | |
| 31 | 31 | if( o && getEditingItem() == null ) { |
| 32 | 32 | getSelectionModel().clearSelection(); |
| 4 | 4 | import javafx.util.StringConverter; |
| 5 | 5 | |
| 6 | import static com.keenwrite.util.Strings.sanitize; | |
| 7 | ||
| 6 | 8 | /** |
| 7 | 9 | * Responsible for converting objects to and from string instances. The |
| ... | ||
| 19 | 21 | public String fromString( final String string ) { |
| 20 | 22 | return sanitize( string ); |
| 21 | } | |
| 22 | ||
| 23 | private String sanitize( final String string ) { | |
| 24 | return string == null ? "" : string; | |
| 25 | 23 | } |
| 26 | 24 | } |
| 1 | /* Copyright 2024 White Magic Software, Ltd. -- All rights reserved. | |
| 2 | * | |
| 3 | * SPDX-License-Identifier: MIT | |
| 4 | */ | |
| 5 | package com.keenwrite.util; | |
| 6 | ||
| 7 | import org.mozilla.universalchardet.UniversalDetector; | |
| 8 | ||
| 9 | import java.nio.charset.Charset; | |
| 10 | ||
| 11 | import static com.keenwrite.constants.Constants.DEFAULT_CHARSET; | |
| 12 | import static java.nio.charset.Charset.forName; | |
| 13 | import static java.util.Locale.ENGLISH; | |
| 14 | ||
| 15 | /** | |
| 16 | * Wraps the {@link UniversalDetector} with to provide enhanced abilities | |
| 17 | * and bug fixes (if needed). | |
| 18 | */ | |
| 19 | public class EncodingDetector { | |
| 20 | ||
| 21 | private final UniversalDetector mDetector; | |
| 22 | ||
| 23 | public EncodingDetector() { | |
| 24 | mDetector = new UniversalDetector( null ); | |
| 25 | } | |
| 26 | ||
| 27 | /** | |
| 28 | * Returns the character set for the constructed input. This will coerce | |
| 29 | * both US-ASCII and TIS620 to UTF-8. | |
| 30 | * | |
| 31 | * @param bytes The textual content having an as yet unknown encoding. | |
| 32 | * @return The character encoding for the given bytes. | |
| 33 | */ | |
| 34 | public Charset detect( final byte[] bytes ) { | |
| 35 | mDetector.handleData( bytes, 0, bytes.length ); | |
| 36 | mDetector.dataEnd(); | |
| 37 | ||
| 38 | final String detectedCharset = mDetector.getDetectedCharset(); | |
| 39 | ||
| 40 | // TODO: Revert when the issue has been fixed. | |
| 41 | // https://github.com/albfernandez/juniversalchardet/issues/35 | |
| 42 | return switch( detectedCharset ) { | |
| 43 | case null -> DEFAULT_CHARSET; | |
| 44 | case "US-ASCII", "TIS620" -> DEFAULT_CHARSET; | |
| 45 | default -> forName( detectedCharset.toUpperCase( ENGLISH ) ); | |
| 46 | }; | |
| 47 | } | |
| 48 | } | |
| 1 | 49 |
| 1 | 1 | package com.keenwrite.encoding; |
| 2 | 2 | |
| 3 | import com.keenwrite.util.EncodingDetector; | |
| 3 | 4 | import org.junit.jupiter.api.Test; |
| 4 | import org.mozilla.universalchardet.UniversalDetector; | |
| 5 | 5 | |
| 6 | 6 | import java.nio.charset.Charset; |
| 7 | 7 | import java.nio.charset.StandardCharsets; |
| 8 | 8 | |
| 9 | import static java.nio.charset.StandardCharsets.*; | |
| 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; |
| 10 | import static org.junit.jupiter.api.Assertions.assertNotNull; | |
| 11 | 11 | |
| 12 | 12 | public class EncodingTest { |
| 13 | 13 | @Test |
| 14 | @SuppressWarnings( "UnnecessaryLocalVariable" ) | |
| 14 | 15 | public void test_Encoding_UTF8_UTF8() { |
| 15 | 16 | final var bytes = testBytes(); |
| 16 | ||
| 17 | final var detector = new UniversalDetector( null ); | |
| 18 | detector.handleData( bytes, 0, bytes.length ); | |
| 19 | detector.dataEnd(); | |
| 20 | ||
| 21 | final var expectedCharset = StandardCharsets.UTF_8; | |
| 22 | final var detectedCharset = detector.getDetectedCharset(); | |
| 23 | ||
| 24 | assertNotNull( detectedCharset ); | |
| 25 | ||
| 26 | final var actualCharset = Charset.forName( detectedCharset ); | |
| 17 | final var detector = new EncodingDetector(); | |
| 18 | final var expectedCharset = UTF_8; | |
| 19 | final var actualCharset = detector.detect( bytes ); | |
| 27 | 20 | |
| 28 | 21 | assertEquals( expectedCharset, actualCharset ); |
| ... | ||
| 42 | 35 | more relaxing. |
| 43 | 36 | """ |
| 44 | .getBytes(); | |
| 37 | .getBytes( UTF_8 ); | |
| 45 | 38 | } |
| 46 | 39 | } |
| 14 | 14 | */ |
| 15 | 15 | class MediaTypeSnifferTest { |
| 16 | ||
| 17 | 16 | @Test |
| 18 | 17 | void test_Read_KnownFileTypes_MediaTypeReturned() |
| 19 | 18 | throws Exception { |
| 20 | 19 | final var clazz = getClass(); |
| 21 | 20 | final var pkgName = clazz.getPackageName(); |
| 22 | 21 | final var dir = pkgName.replace( '.', '/' ); |
| 23 | 22 | |
| 24 | final var urls = clazz.getClassLoader().getResources( dir + "/images" ); | |
| 23 | final var urls = clazz.getClassLoader().getResources( STR."\{dir}/images" ); | |
| 25 | 24 | assertTrue( urls.hasMoreElements() ); |
| 26 | 25 | |
| ... | ||
| 35 | 34 | final var actualExtension = valueFrom( media ).getExtension(); |
| 36 | 35 | final var expectedExtension = getExtension( image.toString() ); |
| 36 | System.out.println( STR."\{image} -> \{media}" ); | |
| 37 | ||
| 37 | 38 | assertEquals( expectedExtension, actualExtension ); |
| 38 | 39 | } |
| 53 | 53 | "https://kroki.io/robots.txt", TEXT_PLAIN, |
| 54 | 54 | "https://place-hold.it/300x500", IMAGE_GIF, |
| 55 | "https://placekitten.com/g/200/300", IMAGE_JPEG, | |
| 55 | "https://loremflickr.com/200/300", IMAGE_JPEG, | |
| 56 | 56 | "https://upload.wikimedia.org/wikipedia/commons/9/9f/Vimlogo.svg", IMAGE_SVG_XML, |
| 57 | 57 | "https://kroki.io//graphviz/svg/eNpLyUwvSizIUHBXqPZIzcnJ17ULzy_KSanlAgB1EAjQ", IMAGE_SVG_XML |
| 58 | 58 | ); |
| 59 | 59 | //@formatter:on |
| 60 | 60 | |
| 61 | 61 | map.forEach( ( k, v ) -> { |
| 62 | 62 | try( var response = open( k ) ) { |
| 63 | System.out.printf( "%s => %s%n", k, v ); | |
| 63 | 64 | assertEquals( v, response.getMediaType() ); |
| 64 | 65 | } catch( final Exception e ) { |
| 16 | 16 | @Test |
| 17 | 17 | void test_Locate_ExistingExecutable_PathFound() { |
| 18 | testFunction( SysFile::locate, "ls", "/usr/bin/ls" ); | |
| 18 | testFunction( SysFile::locate, "env", "/usr/bin/env" ); | |
| 19 | 19 | } |
| 20 | 20 |
| 72 | 72 | XHTML_TEX, |
| 73 | 73 | """ |
| 74 | <html><head><title/><meta content="2" name="count"/></head><body><p>the 👍 emoji</p> | |
| 74 | <html><head><title/><meta charset="UTF-8"/><meta content="2" name="count"/></head><body><p>the 👍 emoji</p> | |
| 75 | 75 | </body></html>""" |
| 76 | 76 | ) |
| 35 | 35 | private static final String PATH_KITTEN_JPG = STR."\{URI_PATH}.jpg"; |
| 36 | 36 | |
| 37 | /** Web server that doles out images. */ | |
| 38 | private static final String PLACEHOLDER = "loremflickr.com"; | |
| 39 | ||
| 37 | 40 | private static final Map<String, String> IMAGES = new LinkedHashMap<>(); |
| 38 | 41 | |
| 39 | 42 | static { |
| 40 | 43 | add( PATH_KITTEN_PNG, URI_PATH ); |
| 41 | 44 | add( PATH_KITTEN_PNG, URI_FILE ); |
| 42 | 45 | add( PATH_KITTEN_PNG, PATH_KITTEN_PNG ); |
| 43 | 46 | add( PATH_KITTEN_JPG, PATH_KITTEN_JPG ); |
| 44 | add( "//placekitten.com/200/200", "//placekitten.com/200/200" ); | |
| 45 | add( "ftp://placekitten.com/200/200", "ftp://placekitten.com/200/200" ); | |
| 46 | add( "http://placekitten.com/200/200", "http://placekitten.com/200/200" ); | |
| 47 | add( "https://placekitten.com/200/200", "https://placekitten.com/200/200" ); | |
| 47 | add( STR."//\{PLACEHOLDER}/200/200", STR."//\{PLACEHOLDER}/200/200" ); | |
| 48 | add( STR."ftp://\{PLACEHOLDER}/200/200", STR."ftp://\{PLACEHOLDER}/200/200" ); | |
| 49 | add( STR."http://\{PLACEHOLDER}/200/200", STR."http://\{PLACEHOLDER}/200/200" ); | |
| 50 | add( STR."https://\{PLACEHOLDER}/200/200", STR."https://\{PLACEHOLDER}/200/200" ); | |
| 48 | 51 | } |
| 49 | 52 |
| 176 | 176 | args( |
| 177 | 177 | """ |
| 178 |  | |
| 178 |  | |
| 179 | 179 | |
| 180 | 180 | :: Caption **bold** {#fig:label} *italics* |
| 181 | 181 | """, |
| 182 | 182 | """ |
| 183 | 183 | <p><span class="caption">Caption <strong>bold</strong> <em>italics</em></span><a class="name" data-type="fig" name="label" /></p> |
| 184 | <p><img src="placekitten" alt="kitteh" /></p> | |
| 184 | <p><img src="kitten" alt="kitteh" /></p> | |
| 185 | 185 | """ |
| 186 | 186 | ), |