| 188 | 188 | test { |
| 189 | 189 | useJUnitPlatform() |
| 190 | ||
| 191 | testLogging { | |
| 192 | exceptionFormat = 'full' | |
| 193 | } | |
| 190 | 194 | } |
| 191 | 195 |
| 3 | 3 | classes: |
| 4 | 4 | processors: |
| 5 | markdown: "MarkdownProcessor" | |
| 5 | markdown: MarkdownProcessor | |
| 6 | 6 | variable: |
| 7 | definition: "DefinitionProcessor" | |
| 8 | preview: "PreviewProcessor" | |
| 7 | definition: DefinitionProcessor | |
| 8 | preview: PreviewProcessor | |
| 9 | 9 | palette: |
| 10 | 10 | primary: |
| 11 | light: "#51a9cf" | |
| 12 | dark: "#126d95" | |
| 11 | light: '#51a9cf' | |
| 12 | dark: '#126d95' | |
| 13 | 13 | secondary: |
| 14 | light: "#ec706a" | |
| 15 | dark: "#7e252f" | |
| 14 | light: '#ec706a' | |
| 15 | dark: '#7e252f' | |
| 16 | 16 | accent: |
| 17 | light: "#76A786" | |
| 18 | dark: "#385742" | |
| 17 | light: '#76A786' | |
| 18 | dark: '#385742' | |
| 19 | 19 | grayscale: |
| 20 | light: "#bac2c5" | |
| 21 | dark: "#394343" | |
| 20 | light: '#bac2c5' | |
| 21 | dark: '#394343' | |
| 22 | 22 | graph: |
| 23 | 23 | label: |
| 24 | 24 | chain: |
| 25 | next: "successor" | |
| 25 | next: successor | |
| 26 | 26 |
| 14 | 14 | * </p> |
| 15 | 15 | */ |
| 16 | public class Bootstrap { | |
| 16 | public final class Bootstrap { | |
| 17 | 17 | private static final Properties BOOTSTRAP = new Properties(); |
| 18 | 18 |
| 183 | 183 | * within the text. Typically this will include the current line number, |
| 184 | 184 | * the number of lines, and the character offset into the text. |
| 185 | * <p> | |
| 186 | * If the {@link Caret} has not been properly built, this will return a | |
| 187 | * string for the status bar having all values set to zero. This can happen | |
| 188 | * during unit testing, but should not happen any other time. | |
| 189 | * </p> | |
| 185 | 190 | * |
| 186 | 191 | * @return A string to present to an end user. |
| 187 | 192 | */ |
| 188 | 193 | @Override |
| 189 | 194 | public String toString() { |
| 190 | return get( STATUS_BAR_LINE, | |
| 191 | getParagraph() + 1, | |
| 192 | getParagraphCount(), | |
| 193 | getTextOffset() + 1 ); | |
| 195 | try { | |
| 196 | return get( STATUS_BAR_LINE, | |
| 197 | getParagraph() + 1, | |
| 198 | getParagraphCount(), | |
| 199 | getTextOffset() + 1 ); | |
| 200 | } catch( final NullPointerException ex ) { | |
| 201 | return get( STATUS_BAR_LINE, 0, 0, 0 ); | |
| 202 | } | |
| 194 | 203 | } |
| 195 | 204 | } |
| 22 | 22 | * Defines application-wide default values. |
| 23 | 23 | */ |
| 24 | public class Constants { | |
| 24 | public final class Constants { | |
| 25 | 25 | |
| 26 | 26 | /** |
| 47 | 47 | } |
| 48 | 48 | else { |
| 49 | editor.replaceText( indexes, operator.entoken( leaf.toPath() ) ); | |
| 49 | final var entokened = operator.entoken( leaf.toPath() ); | |
| 50 | editor.replaceText( indexes, operator.apply( entokened ) ); | |
| 50 | 51 | definitions.expand( leaf ); |
| 51 | 52 | } |
| ... | ||
| 63 | 64 | * @param word Match the word by: exact, beginning, containment, or other. |
| 64 | 65 | */ |
| 65 | @SuppressWarnings("ConstantConditions") | |
| 66 | @SuppressWarnings( "ConstantConditions" ) | |
| 66 | 67 | private static DefinitionTreeItem<String> findLeaf( |
| 67 | 68 | final TextDefinition definition, final String word ) { |
| 13 | 13 | |
| 14 | 14 | /** |
| 15 | * For HTML exports, encode TeX as SVG. | |
| 15 | * For HTML exports, encode TeX as SVG. Treat image links relatively. | |
| 16 | 16 | */ |
| 17 | 17 | HTML_TEX_SVG( ".html" ), |
| 18 | 18 | |
| 19 | 19 | /** |
| 20 | 20 | * For HTML exports, encode TeX using {@code $} delimiters, suitable for |
| 21 | 21 | * rendering by an external TeX typesetting engine (or online with KaTeX). |
| 22 | * Treat image links relatively. | |
| 22 | 23 | */ |
| 23 | 24 | HTML_TEX_DELIMITED( ".html" ), |
| 24 | 25 | |
| 25 | 26 | /** |
| 26 | 27 | * Indicates that the processors should export to a Markdown format. |
| 28 | * Treat image links relatively. | |
| 27 | 29 | */ |
| 28 | 30 | MARKDOWN_PLAIN( ".out.md" ), |
| 29 | 31 | |
| 30 | 32 | /** |
| 31 | 33 | * Indicates no special export format is to be created. No extension is |
| 32 | * applicable. | |
| 34 | * applicable. Image links must use absolute directories. | |
| 33 | 35 | */ |
| 34 | 36 | NONE( "" ); |
| 18 | 18 | * </p> |
| 19 | 19 | */ |
| 20 | public class Launcher { | |
| 20 | public final class Launcher { | |
| 21 | 21 | /** |
| 22 | 22 | * Delegates to the application entry point. |
| 761 | 761 | |
| 762 | 762 | public ProcessorContext createProcessorContext() { |
| 763 | return createProcessorContext( NONE ); | |
| 764 | } | |
| 765 | ||
| 766 | public ProcessorContext createProcessorContext( final ExportFormat format ) { | |
| 763 | 767 | final var editor = getActiveTextEditor(); |
| 764 | return createProcessorContext( editor.getPath(), editor.getCaret() ); | |
| 768 | return createProcessorContext( | |
| 769 | editor.getPath(), editor.getCaret(), format ); | |
| 765 | 770 | } |
| 766 | 771 | |
| ... | ||
| 774 | 779 | */ |
| 775 | 780 | private ProcessorContext createProcessorContext( |
| 776 | final Path path, final Caret caret ) { | |
| 781 | final Path path, final Caret caret, final ExportFormat format ) { | |
| 777 | 782 | return new ProcessorContext( |
| 778 | mHtmlPreview, mResolvedMap, path, caret, NONE, mWorkspace | |
| 783 | mHtmlPreview, mResolvedMap, path, caret, format, mWorkspace | |
| 779 | 784 | ); |
| 780 | 785 | } |
| ... | ||
| 800 | 805 | final var editor = new MarkdownEditor( file, getWorkspace() ); |
| 801 | 806 | final var caret = editor.getCaret(); |
| 802 | final var context = createProcessorContext( path, caret ); | |
| 807 | final var context = createProcessorContext( path, caret, NONE ); | |
| 803 | 808 | |
| 804 | 809 | mProcessors.computeIfAbsent( editor, p -> createProcessors( context ) ); |
| 20 | 20 | * Responsible for creating the bar scene: menu bar, tool bar, and status bar. |
| 21 | 21 | */ |
| 22 | public class MainScene { | |
| 22 | public final class MainScene { | |
| 23 | 23 | private final Scene mScene; |
| 24 | 24 | private final Node mMenuBar; |
| 16 | 16 | * properties using a <code>${var}</code> syntax. |
| 17 | 17 | */ |
| 18 | public class Messages { | |
| 18 | public final class Messages { | |
| 19 | 19 | |
| 20 | 20 | private static final ResourceBundle RESOURCE_BUNDLE = |
| 15 | 15 | * class can go away. |
| 16 | 16 | */ |
| 17 | public class StatusNotifier { | |
| 17 | public final class StatusNotifier { | |
| 18 | 18 | private static final String OK = get( STATUS_BAR_OK, "OK" ); |
| 19 | 19 |
| 99 | 99 | * Constructs a definition pane with a given tree view root. |
| 100 | 100 | * |
| 101 | * @param file The file to | |
| 101 | * @param file The file of definitions to maintain through the UI. | |
| 102 | 102 | */ |
| 103 | 103 | public DefinitionEditor( |
| 22 | 22 | * @author White Magic Software, Ltd. |
| 23 | 23 | */ |
| 24 | public class DefinitionTabSceneFactory { | |
| 24 | public final class DefinitionTabSceneFactory { | |
| 25 | 25 | |
| 26 | 26 | private final Consumer<Tab> mTabSelectionConsumer; |
| 22 | 22 | * and respond to drag and drop functionality. |
| 23 | 23 | */ |
| 24 | public class TreeCellFactory | |
| 24 | public final class TreeCellFactory | |
| 25 | 25 | implements Callback<TreeView<String>, TreeCell<String>> { |
| 26 | 26 | private static final String STYLE_CLASS_DROP_TARGET = "drop-target"; |
| 32 | 32 | * </p> |
| 33 | 33 | */ |
| 34 | public class TreeItemMapper { | |
| 34 | public final class TreeItemMapper { | |
| 35 | 35 | /** |
| 36 | 36 | * Separates definition keys (e.g., the dots in {@code $root.node.var$}). |
| 4 | 4 | import javafx.scene.control.TreeItem; |
| 5 | 5 | |
| 6 | import java.util.function.Consumer; | |
| 7 | import java.util.function.Function; | |
| 8 | ||
| 9 | 6 | /** |
| 10 | 7 | * Responsible for converting an object hierarchy into a {@link TreeItem} |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.editors.definition.yaml; | |
| 3 | ||
| 4 | import com.fasterxml.jackson.databind.JsonNode; | |
| 5 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 6 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; | |
| 7 | ||
| 8 | import java.util.function.Function; | |
| 9 | ||
| 10 | /** | |
| 11 | * Responsible for reading a YAML document into an object hierarchy. | |
| 12 | */ | |
| 13 | class YamlParser implements Function<String, JsonNode> { | |
| 14 | ||
| 15 | /** | |
| 16 | * Creates a new instance that can parse the contents of a YAML | |
| 17 | * document. | |
| 18 | */ | |
| 19 | YamlParser() { | |
| 20 | } | |
| 21 | ||
| 22 | @Override | |
| 23 | public JsonNode apply( final String yaml ) { | |
| 24 | try { | |
| 25 | return new ObjectMapper( new YAMLFactory() ).readTree( yaml ); | |
| 26 | } catch( final Exception ex ) { | |
| 27 | // Ensure that a document root node exists. | |
| 28 | return new ObjectMapper().createObjectNode(); | |
| 29 | } | |
| 30 | } | |
| 31 | } | |
| 32 | 1 |
| 3 | 3 | |
| 4 | 4 | import com.fasterxml.jackson.databind.JsonNode; |
| 5 | import com.fasterxml.jackson.databind.ObjectMapper; | |
| 5 | 6 | import com.fasterxml.jackson.databind.node.ObjectNode; |
| 7 | import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; | |
| 6 | 8 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; |
| 7 | 9 | import com.keenwrite.editors.definition.DefinitionTreeItem; |
| 8 | 10 | import com.keenwrite.editors.definition.TreeTransformer; |
| 9 | 11 | import javafx.scene.control.TreeItem; |
| 10 | 12 | import javafx.scene.control.TreeView; |
| 11 | 13 | |
| 12 | 14 | import java.util.Map.Entry; |
| 15 | ||
| 16 | import static com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.MINIMIZE_QUOTES; | |
| 17 | import static com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature.SPLIT_LINES; | |
| 13 | 18 | |
| 14 | 19 | /** |
| 15 | 20 | * Transforms a JsonNode hierarchy into a tree that can be displayed in a user |
| 16 | 21 | * interface and vice-versa. |
| 17 | 22 | */ |
| 18 | 23 | public final class YamlTreeTransformer implements TreeTransformer { |
| 24 | private static final YAMLFactory sFactory; | |
| 25 | private static final YAMLMapper sMapper; | |
| 26 | ||
| 27 | static { | |
| 28 | sFactory = new YAMLFactory(); | |
| 29 | sFactory.configure( MINIMIZE_QUOTES, true ); | |
| 30 | sFactory.configure( SPLIT_LINES, false ); | |
| 31 | sMapper = new YAMLMapper( sFactory ); | |
| 32 | } | |
| 19 | 33 | |
| 20 | 34 | /** |
| ... | ||
| 28 | 42 | public String transform( final TreeItem<String> treeItem ) { |
| 29 | 43 | try { |
| 30 | final YAMLMapper mapper = new YAMLMapper(); | |
| 31 | final ObjectNode root = mapper.createObjectNode(); | |
| 44 | final var root = sMapper.createObjectNode(); | |
| 32 | 45 | |
| 33 | 46 | // Iterate over the root item's children. The root item is used by the |
| 34 | 47 | // application to ensure definitions can always be added to a tree, as |
| 35 | 48 | // such it is not meant to be exported, only its children. |
| 36 | for( final TreeItem<String> child : treeItem.getChildren() ) { | |
| 49 | for( final var child : treeItem.getChildren() ) { | |
| 37 | 50 | transform( child, root ); |
| 38 | 51 | } |
| 39 | 52 | |
| 40 | return mapper.writeValueAsString( root ); | |
| 53 | return sMapper.writeValueAsString( root ); | |
| 41 | 54 | } catch( final Exception ex ) { |
| 42 | 55 | throw new RuntimeException( ex ); |
| 56 | } | |
| 57 | } | |
| 58 | ||
| 59 | /** | |
| 60 | * Converts a YAML document to a {@link TreeItem} based on the document | |
| 61 | * keys. | |
| 62 | * | |
| 63 | * @param document The YAML document to convert to a hierarchy of | |
| 64 | * {@link TreeItem} instances. | |
| 65 | * @throws StackOverflowError If infinite recursion is encountered. | |
| 66 | */ | |
| 67 | @Override | |
| 68 | public TreeItem<String> transform( final String document ) { | |
| 69 | final var jsonNode = toJson( document ); | |
| 70 | final var rootItem = createTreeItem( "root" ); | |
| 71 | ||
| 72 | transform( jsonNode, rootItem ); | |
| 73 | ||
| 74 | return rootItem; | |
| 75 | } | |
| 76 | ||
| 77 | private JsonNode toJson( final String yaml ) { | |
| 78 | try { | |
| 79 | return new ObjectMapper( sFactory ).readTree( yaml ); | |
| 80 | } catch( final Exception ex ) { | |
| 81 | // Ensure that a document root node exists. | |
| 82 | return new ObjectMapper().createObjectNode(); | |
| 43 | 83 | } |
| 44 | 84 | } |
| ... | ||
| 69 | 109 | } |
| 70 | 110 | } |
| 71 | } | |
| 72 | ||
| 73 | /** | |
| 74 | * Converts a YAML document to a {@link TreeItem} based on the document | |
| 75 | * keys. | |
| 76 | * | |
| 77 | * @param document The YAML document to convert to a hierarchy of | |
| 78 | * {@link TreeItem} instances. | |
| 79 | * @throws StackOverflowError If infinite recursion is encountered. | |
| 80 | */ | |
| 81 | @Override | |
| 82 | public TreeItem<String> transform( final String document ) { | |
| 83 | final var parser = new YamlParser(); | |
| 84 | final var jsonNode = parser.apply( document ); | |
| 85 | final var rootItem = createTreeItem( "root" ); | |
| 86 | ||
| 87 | transform( jsonNode, rootItem ); | |
| 88 | ||
| 89 | return rootItem; | |
| 90 | 111 | } |
| 91 | 112 | |
| ... | ||
| 110 | 131 | */ |
| 111 | 132 | private void transform( |
| 112 | final Entry<String, JsonNode> node, final TreeItem<String> item ) { | |
| 133 | final Entry<String, JsonNode> node, final TreeItem<String> item ) { | |
| 113 | 134 | final var leafNode = node.getValue(); |
| 114 | 135 | final var key = node.getKey(); |
| 7 | 7 | * Represents the model for a hyperlink: text, url, and title. |
| 8 | 8 | */ |
| 9 | public class HyperlinkModel { | |
| 9 | public final class HyperlinkModel { | |
| 10 | 10 | |
| 11 | 11 | private String text; |
| 37 | 37 | * can edit the link within a dialog. |
| 38 | 38 | */ |
| 39 | public class LinkVisitor { | |
| 39 | public final class LinkVisitor { | |
| 40 | 40 | |
| 41 | 41 | private NodeVisitor mVisitor; |
| 54 | 54 | * Responsible for editing Markdown documents. |
| 55 | 55 | */ |
| 56 | public class MarkdownEditor extends BorderPane implements TextEditor { | |
| 56 | public final class MarkdownEditor extends BorderPane implements TextEditor { | |
| 57 | 57 | /** |
| 58 | 58 | * Regular expression that matches the type of markup block. This is used |
| 10 | 10 | * This avoids duplicating the error message prefix. |
| 11 | 11 | */ |
| 12 | public class MissingFileException extends FileNotFoundException { | |
| 12 | public final class MissingFileException extends FileNotFoundException { | |
| 13 | 13 | /** |
| 14 | 14 | * Constructs a new {@link MissingFileException} using the given path. |
| 19 | 19 | * an HTTP request. |
| 20 | 20 | */ |
| 21 | public class HttpMediaType { | |
| 21 | public final class HttpMediaType { | |
| 22 | 22 | |
| 23 | 23 | private final static HttpClient HTTP_CLIENT = HttpClient |
| 13 | 13 | * of string comparisons, including basic strings and file name strings. |
| 14 | 14 | */ |
| 15 | public class PredicateFactory { | |
| 15 | public final class PredicateFactory { | |
| 16 | 16 | /** |
| 17 | 17 | * Creates an instance of {@link Predicate} that matches a globbed file |
| 6 | 6 | import java.io.File; |
| 7 | 7 | |
| 8 | public class FileProperty extends SimpleObjectProperty<File> { | |
| 8 | public final class FileProperty extends SimpleObjectProperty<File> { | |
| 9 | 9 | public FileProperty( final File file ) { |
| 10 | 10 | super( file ); |
| 14 | 14 | import static java.util.Locale.forLanguageTag; |
| 15 | 15 | |
| 16 | public class LocaleProperty extends SimpleObjectProperty<String> { | |
| 16 | public final class LocaleProperty extends SimpleObjectProperty<String> { | |
| 17 | 17 | |
| 18 | 18 | /** |
| 14 | 14 | * using the same format. |
| 15 | 15 | */ |
| 16 | public class LocaleScripts { | |
| 16 | public final class LocaleScripts { | |
| 17 | 17 | /** |
| 18 | 18 | * ISO 15924 alpha-4 script code to represent Latin scripts. |
| 35 | 35 | */ |
| 36 | 36 | @SuppressWarnings( "SameParameterValue" ) |
| 37 | public class PreferencesController { | |
| 37 | public final class PreferencesController { | |
| 38 | 38 | |
| 39 | 39 | private final Workspace mWorkspace; |
| 60 | 60 | * </dl> |
| 61 | 61 | */ |
| 62 | public class Workspace { | |
| 63 | private static final Key KEY_ROOT = key( "workspace" ); | |
| 64 | ||
| 65 | public static final Key KEY_META = key( KEY_ROOT, "meta" ); | |
| 66 | public static final Key KEY_META_NAME = key( KEY_META, "name" ); | |
| 67 | public static final Key KEY_META_VERSION = key( KEY_META, "version" ); | |
| 68 | ||
| 69 | public static final Key KEY_R = key( KEY_ROOT, "r" ); | |
| 70 | public static final Key KEY_R_SCRIPT = key( KEY_R, "script" ); | |
| 71 | public static final Key KEY_R_DIR = key( KEY_R, "dir" ); | |
| 72 | public static final Key KEY_R_DELIM = key( KEY_R, "delimiter" ); | |
| 73 | public static final Key KEY_R_DELIM_BEGAN = key( KEY_R_DELIM, "began" ); | |
| 74 | public static final Key KEY_R_DELIM_ENDED = key( KEY_R_DELIM, "ended" ); | |
| 75 | ||
| 76 | public static final Key KEY_IMAGES = key( KEY_ROOT, "images" ); | |
| 77 | public static final Key KEY_IMAGES_DIR = key( KEY_IMAGES, "dir" ); | |
| 78 | public static final Key KEY_IMAGES_ORDER = key( KEY_IMAGES, "order" ); | |
| 79 | ||
| 80 | public static final Key KEY_DEF = key( KEY_ROOT, "definition" ); | |
| 81 | public static final Key KEY_DEF_PATH = key( KEY_DEF, "path" ); | |
| 82 | public static final Key KEY_DEF_DELIM = key( KEY_DEF, "delimiter" ); | |
| 83 | public static final Key KEY_DEF_DELIM_BEGAN = key( KEY_DEF_DELIM, "began" ); | |
| 84 | public static final Key KEY_DEF_DELIM_ENDED = key( KEY_DEF_DELIM, "ended" ); | |
| 85 | ||
| 86 | //@formatter:off | |
| 87 | public static final Key KEY_UI = key( KEY_ROOT, "ui" ); | |
| 88 | ||
| 89 | public static final Key KEY_UI_RECENT = key( KEY_UI, "recent" ); | |
| 90 | public static final Key KEY_UI_RECENT_DIR = key( KEY_UI_RECENT, "dir" ); | |
| 91 | public static final Key KEY_UI_RECENT_DOCUMENT = key( KEY_UI_RECENT,"document" ); | |
| 92 | public static final Key KEY_UI_RECENT_DEFINITION = key( KEY_UI_RECENT, "definition" ); | |
| 93 | ||
| 94 | public static final Key KEY_UI_FILES = key( KEY_UI, "files" ); | |
| 95 | public static final Key KEY_UI_FILES_PATH = key( KEY_UI_FILES, "path" ); | |
| 96 | ||
| 97 | public static final Key KEY_UI_FONT = key( KEY_UI, "font" ); | |
| 98 | public static final Key KEY_UI_FONT_EDITOR = key( KEY_UI_FONT, "editor" ); | |
| 99 | public static final Key KEY_UI_FONT_EDITOR_NAME = key( KEY_UI_FONT_EDITOR, "name" ); | |
| 100 | public static final Key KEY_UI_FONT_EDITOR_SIZE = key( KEY_UI_FONT_EDITOR, "size" ); | |
| 101 | public static final Key KEY_UI_FONT_PREVIEW = key( KEY_UI_FONT, "preview" ); | |
| 102 | public static final Key KEY_UI_FONT_PREVIEW_NAME = key( KEY_UI_FONT_PREVIEW, "name" ); | |
| 103 | public static final Key KEY_UI_FONT_PREVIEW_SIZE = key( KEY_UI_FONT_PREVIEW, "size" ); | |
| 104 | public static final Key KEY_UI_FONT_PREVIEW_MONO = key( KEY_UI_FONT_PREVIEW, "mono" ); | |
| 105 | public static final Key KEY_UI_FONT_PREVIEW_MONO_NAME = key( KEY_UI_FONT_PREVIEW_MONO, "name" ); | |
| 106 | public static final Key KEY_UI_FONT_PREVIEW_MONO_SIZE = key( KEY_UI_FONT_PREVIEW_MONO, "size" ); | |
| 107 | ||
| 108 | public static final Key KEY_LANGUAGE = key( KEY_ROOT, "language" ); | |
| 109 | public static final Key KEY_LANG_LOCALE = key( KEY_LANGUAGE, "locale" ); | |
| 110 | ||
| 111 | public static final Key KEY_UI_WINDOW = key( KEY_UI, "window" ); | |
| 112 | public static final Key KEY_UI_WINDOW_X = key( KEY_UI_WINDOW, "x" ); | |
| 113 | public static final Key KEY_UI_WINDOW_Y = key( KEY_UI_WINDOW, "y" ); | |
| 114 | public static final Key KEY_UI_WINDOW_W = key( KEY_UI_WINDOW, "width" ); | |
| 115 | public static final Key KEY_UI_WINDOW_H = key( KEY_UI_WINDOW, "height" ); | |
| 116 | public static final Key KEY_UI_WINDOW_MAX = key( KEY_UI_WINDOW, "maximized" ); | |
| 117 | public static final Key KEY_UI_WINDOW_FULL = key( KEY_UI_WINDOW, "full" ); | |
| 118 | ||
| 119 | private final Map<Key, Property<?>> VALUES = Map.ofEntries( | |
| 120 | entry( KEY_META_VERSION, asStringProperty( getVersion() ) ), | |
| 121 | entry( KEY_META_NAME, asStringProperty( "default" ) ), | |
| 122 | ||
| 123 | entry( KEY_R_SCRIPT, asStringProperty( "" ) ), | |
| 124 | entry( KEY_R_DIR, asFileProperty( USER_DIRECTORY ) ), | |
| 125 | entry( KEY_R_DELIM_BEGAN, asStringProperty( R_DELIM_BEGAN_DEFAULT ) ), | |
| 126 | entry( KEY_R_DELIM_ENDED, asStringProperty( R_DELIM_ENDED_DEFAULT ) ), | |
| 127 | ||
| 128 | entry( KEY_IMAGES_DIR, asFileProperty( USER_DIRECTORY ) ), | |
| 129 | entry( KEY_IMAGES_ORDER, asStringProperty( PERSIST_IMAGES_DEFAULT ) ), | |
| 130 | ||
| 131 | entry( KEY_DEF_PATH, asFileProperty( DEFINITION_DEFAULT ) ), | |
| 132 | entry( KEY_DEF_DELIM_BEGAN, asStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ), | |
| 133 | entry( KEY_DEF_DELIM_ENDED, asStringProperty( DEF_DELIM_ENDED_DEFAULT ) ), | |
| 134 | ||
| 135 | entry( KEY_UI_RECENT_DIR, asFileProperty( USER_DIRECTORY ) ), | |
| 136 | entry( KEY_UI_RECENT_DOCUMENT, asFileProperty( DOCUMENT_DEFAULT ) ), | |
| 137 | entry( KEY_UI_RECENT_DEFINITION, asFileProperty( DEFINITION_DEFAULT ) ), | |
| 138 | ||
| 139 | entry( KEY_LANG_LOCALE, asLocaleProperty( LOCALE_DEFAULT ) ), | |
| 140 | entry( KEY_UI_FONT_EDITOR_NAME, asStringProperty( FONT_NAME_EDITOR_DEFAULT ) ), | |
| 141 | entry( KEY_UI_FONT_EDITOR_SIZE, asDoubleProperty( FONT_SIZE_EDITOR_DEFAULT ) ), | |
| 142 | entry( KEY_UI_FONT_PREVIEW_NAME, asStringProperty( FONT_NAME_PREVIEW_DEFAULT ) ), | |
| 143 | entry( KEY_UI_FONT_PREVIEW_SIZE, asDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT ) ), | |
| 144 | entry( KEY_UI_FONT_PREVIEW_MONO_NAME, asStringProperty( FONT_NAME_PREVIEW_MONO_NAME_DEFAULT ) ), | |
| 145 | entry( KEY_UI_FONT_PREVIEW_MONO_SIZE, asDoubleProperty( FONT_SIZE_PREVIEW_MONO_SIZE_DEFAULT ) ), | |
| 146 | ||
| 147 | entry( KEY_UI_WINDOW_X, asDoubleProperty( WINDOW_X_DEFAULT ) ), | |
| 148 | entry( KEY_UI_WINDOW_Y, asDoubleProperty( WINDOW_Y_DEFAULT ) ), | |
| 149 | entry( KEY_UI_WINDOW_W, asDoubleProperty( WINDOW_W_DEFAULT ) ), | |
| 150 | entry( KEY_UI_WINDOW_H, asDoubleProperty( WINDOW_H_DEFAULT ) ), | |
| 151 | entry( KEY_UI_WINDOW_MAX, asBooleanProperty() ), | |
| 152 | entry( KEY_UI_WINDOW_FULL, asBooleanProperty() ) | |
| 153 | ); | |
| 154 | //@formatter:on | |
| 155 | ||
| 156 | private StringProperty asStringProperty( final String defaultValue ) { | |
| 157 | return new SimpleStringProperty( defaultValue ); | |
| 158 | } | |
| 159 | ||
| 160 | private DoubleProperty asDoubleProperty( final double defaultValue ) { | |
| 161 | return new SimpleDoubleProperty( defaultValue ); | |
| 162 | } | |
| 163 | ||
| 164 | private BooleanProperty asBooleanProperty() { | |
| 165 | return new SimpleBooleanProperty(); | |
| 166 | } | |
| 167 | ||
| 168 | private FileProperty asFileProperty( final File defaultValue ) { | |
| 169 | return new FileProperty( defaultValue ); | |
| 170 | } | |
| 171 | ||
| 172 | @SuppressWarnings( "SameParameterValue" ) | |
| 173 | private LocaleProperty asLocaleProperty( final Locale defaultValue ) { | |
| 174 | return new LocaleProperty( defaultValue ); | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Helps instantiate {@link Property} instances for XML configuration items. | |
| 179 | */ | |
| 180 | private static final Map<Class<?>, Function<String, Object>> UNMARSHALL = | |
| 181 | Map.of( | |
| 182 | LocaleProperty.class, LocaleProperty::parseLocale, | |
| 183 | SimpleBooleanProperty.class, Boolean::parseBoolean, | |
| 184 | SimpleDoubleProperty.class, Double::parseDouble, | |
| 185 | SimpleFloatProperty.class, Float::parseFloat, | |
| 186 | FileProperty.class, File::new | |
| 187 | ); | |
| 188 | ||
| 189 | private static final Map<Class<?>, Function<String, Object>> MARSHALL = | |
| 190 | Map.of( | |
| 191 | LocaleProperty.class, LocaleProperty::toLanguageTag | |
| 192 | ); | |
| 193 | ||
| 194 | private final Map<Key, SetProperty<?>> SETS = Map.ofEntries( | |
| 195 | entry( | |
| 196 | KEY_UI_FILES_PATH, | |
| 197 | new SimpleSetProperty<>( observableSet( new HashSet<>() ) ) | |
| 198 | ) | |
| 199 | ); | |
| 200 | ||
| 201 | /** | |
| 202 | * Creates a new {@link Workspace} that will attempt to load a configuration | |
| 203 | * file. If the configuration file cannot be loaded, the workspace settings | |
| 204 | * will return default values. This allows unit tests to provide an instance | |
| 205 | * of {@link Workspace} when necessary without encountering failures. | |
| 206 | */ | |
| 207 | public Workspace() { | |
| 208 | load(); | |
| 209 | } | |
| 210 | ||
| 211 | /** | |
| 212 | * Creates an instance of {@link ObservableList} that is based on a | |
| 213 | * modifiable observable array list for the given items. | |
| 214 | * | |
| 215 | * @param items The items to wrap in an observable list. | |
| 216 | * @param <E> The type of items to add to the list. | |
| 217 | * @return An observable property that can have its contents modified. | |
| 218 | */ | |
| 219 | public static <E> ObservableList<E> listProperty( final Set<E> items ) { | |
| 220 | return new SimpleListProperty<>( observableArrayList( items ) ); | |
| 221 | } | |
| 222 | ||
| 223 | /** | |
| 224 | * Returns a value that represents a setting in the application that the user | |
| 225 | * may configure, either directly or indirectly. | |
| 226 | * | |
| 227 | * @param key The reference to the users' preference stored in deference | |
| 228 | * of app reëntrance. | |
| 229 | * @return An observable property to be persisted. | |
| 230 | */ | |
| 231 | @SuppressWarnings( "unchecked" ) | |
| 232 | public <T, U extends Property<T>> U valuesProperty( final Key key ) { | |
| 233 | // The type that goes into the map must come out. | |
| 234 | return (U) VALUES.get( key ); | |
| 235 | } | |
| 236 | ||
| 237 | /** | |
| 238 | * Returns a list of values that represent a setting in the application that | |
| 239 | * the user may configure, either directly or indirectly. The property | |
| 240 | * returned is backed by a mutable {@link Set}. | |
| 241 | * | |
| 242 | * @param key The {@link Key} associated with a preference value. | |
| 243 | * @return An observable property to be persisted. | |
| 244 | */ | |
| 245 | @SuppressWarnings( "unchecked" ) | |
| 246 | public <T> SetProperty<T> setsProperty( final Key key ) { | |
| 247 | // The type that goes into the map must come out. | |
| 248 | return (SetProperty<T>) SETS.get( key ); | |
| 249 | } | |
| 250 | ||
| 251 | /** | |
| 252 | * Returns the {@link Boolean} preference value associated with the given | |
| 253 | * {@link Key}. The caller must be sure that the given {@link Key} is | |
| 254 | * associated with a value that matches the return type. | |
| 255 | * | |
| 256 | * @param key The {@link Key} associated with a preference value. | |
| 257 | * @return The value associated with the given {@link Key}. | |
| 258 | */ | |
| 259 | public boolean toBoolean( final Key key ) { | |
| 260 | return (Boolean) valuesProperty( key ).getValue(); | |
| 261 | } | |
| 262 | ||
| 263 | /** | |
| 264 | * Returns the {@link Double} preference value associated with the given | |
| 265 | * {@link Key}. The caller must be sure that the given {@link Key} is | |
| 266 | * associated with a value that matches the return type. | |
| 267 | * | |
| 268 | * @param key The {@link Key} associated with a preference value. | |
| 269 | * @return The value associated with the given {@link Key}. | |
| 270 | */ | |
| 271 | public double toDouble( final Key key ) { | |
| 272 | return (Double) valuesProperty( key ).getValue(); | |
| 273 | } | |
| 274 | ||
| 275 | public File toFile( final Key key ) { | |
| 276 | return fileProperty( key ).get(); | |
| 277 | } | |
| 278 | ||
| 279 | public String toString( final Key key ) { | |
| 280 | return stringProperty( key ).get(); | |
| 281 | } | |
| 282 | ||
| 283 | public Tokens toTokens( final Key began, final Key ended ) { | |
| 284 | return new Tokens( stringProperty( began ), stringProperty( ended ) ); | |
| 285 | } | |
| 286 | ||
| 287 | @SuppressWarnings( "SameParameterValue" ) | |
| 288 | public DoubleProperty doubleProperty( final Key key ) { | |
| 289 | return valuesProperty( key ); | |
| 290 | } | |
| 291 | ||
| 292 | /** | |
| 293 | * Returns the {@link File} {@link Property} associated with the given | |
| 294 | * {@link Key} from the internal list of preference values. The caller | |
| 295 | * must be sure that the given {@link Key} is associated with a {@link File} | |
| 296 | * {@link Property}. | |
| 297 | * | |
| 298 | * @param key The {@link Key} associated with a preference value. | |
| 299 | * @return The value associated with the given {@link Key}. | |
| 300 | */ | |
| 301 | public ObjectProperty<File> fileProperty( final Key key ) { | |
| 302 | return valuesProperty( key ); | |
| 303 | } | |
| 304 | ||
| 305 | public LocaleProperty localeProperty( final Key key ) { | |
| 306 | return valuesProperty( key ); | |
| 307 | } | |
| 308 | ||
| 309 | /** | |
| 310 | * Returns the language locale setting for the {@link #KEY_LANG_LOCALE} key. | |
| 311 | * | |
| 312 | * @return The user's current locale setting. | |
| 313 | */ | |
| 314 | public Locale getLocale() { | |
| 315 | return localeProperty( KEY_LANG_LOCALE ).toLocale(); | |
| 316 | } | |
| 317 | ||
| 318 | public StringProperty stringProperty( final Key key ) { | |
| 319 | return valuesProperty( key ); | |
| 320 | } | |
| 321 | ||
| 322 | public void loadValueKeys( final Consumer<Key> consumer ) { | |
| 323 | VALUES.keySet().forEach( consumer ); | |
| 324 | } | |
| 325 | ||
| 326 | public void loadSetKeys( final Consumer<Key> consumer ) { | |
| 327 | SETS.keySet().forEach( consumer ); | |
| 328 | } | |
| 329 | ||
| 330 | /** | |
| 331 | * Calls the given consumer for all single-value keys. For lists, see | |
| 332 | * {@link #saveSets(BiConsumer)}. | |
| 333 | * | |
| 334 | * @param consumer Called to accept each preference key value. | |
| 335 | */ | |
| 336 | public void saveValues( final BiConsumer<Key, Property<?>> consumer ) { | |
| 337 | VALUES.forEach( consumer ); | |
| 338 | } | |
| 339 | ||
| 340 | /** | |
| 341 | * Calls the given consumer for all multi-value keys. For single items, see | |
| 342 | * {@link #saveValues(BiConsumer)}. Callers are responsible for iterating | |
| 343 | * over the list of items retrieved through this method. | |
| 344 | * | |
| 345 | * @param consumer Called to accept each preference key list. | |
| 346 | */ | |
| 347 | public void saveSets( final BiConsumer<Key, SetProperty<?>> consumer ) { | |
| 348 | SETS.forEach( consumer ); | |
| 349 | } | |
| 350 | ||
| 351 | /** | |
| 352 | * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)}, | |
| 353 | * providing a value of {@code true} for the {@link BooleanSupplier} to | |
| 354 | * indicate the property changes always take effect. | |
| 355 | * | |
| 356 | * @param key The value to bind to the internal key property. | |
| 357 | * @param property The external property value that sets the internal value. | |
| 358 | */ | |
| 359 | public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) { | |
| 360 | listen( key, property, () -> true ); | |
| 361 | } | |
| 362 | ||
| 363 | /** | |
| 364 | * Binds a read-only property to a value in the preferences. This allows | |
| 365 | * user interface properties to change and the preferences will be | |
| 366 | * synchronized automatically. | |
| 367 | * <p> | |
| 368 | * This calls {@link Platform#runLater(Runnable)} to ensure that all pending | |
| 369 | * application window states are finished before assessing whether property | |
| 370 | * changes should be applied. Without this, exiting the application while the | |
| 371 | * window is maximized would persist the window's maximum dimensions, | |
| 372 | * preventing restoration to its prior, non-maximum size. | |
| 373 | * </p> | |
| 374 | * | |
| 375 | * @param key The value to bind to the internal key property. | |
| 376 | * @param property The external property value that sets the internal value. | |
| 377 | * @param enabled Indicates whether property changes should be applied. | |
| 378 | */ | |
| 379 | public <T> void listen( | |
| 380 | final Key key, | |
| 381 | final ReadOnlyProperty<T> property, | |
| 382 | final BooleanSupplier enabled ) { | |
| 383 | property.addListener( | |
| 384 | ( c, o, n ) -> runLater( () -> { | |
| 385 | if( enabled.getAsBoolean() ) { | |
| 386 | valuesProperty( key ).setValue( n ); | |
| 387 | } | |
| 388 | } ) | |
| 389 | ); | |
| 390 | } | |
| 391 | ||
| 392 | /** | |
| 393 | * Saves the current workspace. | |
| 394 | */ | |
| 395 | public void save() { | |
| 396 | try { | |
| 397 | final var config = new XMLConfiguration(); | |
| 398 | ||
| 399 | // The root config key can only be set for an empty configuration file. | |
| 400 | config.setRootElementName( APP_TITLE_LOWERCASE ); | |
| 401 | valuesProperty( KEY_META_VERSION ).setValue( getVersion() ); | |
| 402 | ||
| 403 | saveValues( ( key, property ) -> | |
| 404 | config.setProperty( key.toString(), marshall( property ) ) | |
| 405 | ); | |
| 406 | ||
| 407 | saveSets( ( key, set ) -> { | |
| 408 | final var keyName = key.toString(); | |
| 409 | set.forEach( ( value ) -> config.addProperty( keyName, value ) ); | |
| 410 | } ); | |
| 411 | new FileHandler( config ).save( FILE_PREFERENCES ); | |
| 412 | } catch( final Exception ex ) { | |
| 413 | clue( ex ); | |
| 414 | } | |
| 415 | } | |
| 416 | ||
| 417 | /** | |
| 418 | * Attempts to load the {@link Constants#FILE_PREFERENCES} configuration file. | |
| 419 | * If not found, this will fall back to an empty configuration file, leaving | |
| 420 | * the application to fill in default values. | |
| 421 | */ | |
| 422 | private void load() { | |
| 423 | try { | |
| 424 | final var config = new Configurations().xml( FILE_PREFERENCES ); | |
| 62 | public final class Workspace { | |
| 63 | private static final Key KEY_ROOT = key( "workspace" ); | |
| 64 | ||
| 65 | public static final Key KEY_META = key( KEY_ROOT, "meta" ); | |
| 66 | public static final Key KEY_META_NAME = key( KEY_META, "name" ); | |
| 67 | public static final Key KEY_META_VERSION = key( KEY_META, "version" ); | |
| 68 | ||
| 69 | public static final Key KEY_R = key( KEY_ROOT, "r" ); | |
| 70 | public static final Key KEY_R_SCRIPT = key( KEY_R, "script" ); | |
| 71 | public static final Key KEY_R_DIR = key( KEY_R, "dir" ); | |
| 72 | public static final Key KEY_R_DELIM = key( KEY_R, "delimiter" ); | |
| 73 | public static final Key KEY_R_DELIM_BEGAN = key( KEY_R_DELIM, "began" ); | |
| 74 | public static final Key KEY_R_DELIM_ENDED = key( KEY_R_DELIM, "ended" ); | |
| 75 | ||
| 76 | public static final Key KEY_IMAGES = key( KEY_ROOT, "images" ); | |
| 77 | public static final Key KEY_IMAGES_DIR = key( KEY_IMAGES, "dir" ); | |
| 78 | public static final Key KEY_IMAGES_ORDER = key( KEY_IMAGES, "order" ); | |
| 79 | ||
| 80 | public static final Key KEY_DEF = key( KEY_ROOT, "definition" ); | |
| 81 | public static final Key KEY_DEF_PATH = key( KEY_DEF, "path" ); | |
| 82 | public static final Key KEY_DEF_DELIM = key( KEY_DEF, "delimiter" ); | |
| 83 | public static final Key KEY_DEF_DELIM_BEGAN = key( KEY_DEF_DELIM, "began" ); | |
| 84 | public static final Key KEY_DEF_DELIM_ENDED = key( KEY_DEF_DELIM, "ended" ); | |
| 85 | ||
| 86 | //@formatter:off | |
| 87 | public static final Key KEY_UI = key( KEY_ROOT, "ui" ); | |
| 88 | ||
| 89 | public static final Key KEY_UI_RECENT = key( KEY_UI, "recent" ); | |
| 90 | public static final Key KEY_UI_RECENT_DIR = key( KEY_UI_RECENT, "dir" ); | |
| 91 | public static final Key KEY_UI_RECENT_DOCUMENT = key( KEY_UI_RECENT,"document" ); | |
| 92 | public static final Key KEY_UI_RECENT_DEFINITION = key( KEY_UI_RECENT, "definition" ); | |
| 93 | ||
| 94 | public static final Key KEY_UI_FILES = key( KEY_UI, "files" ); | |
| 95 | public static final Key KEY_UI_FILES_PATH = key( KEY_UI_FILES, "path" ); | |
| 96 | ||
| 97 | public static final Key KEY_UI_FONT = key( KEY_UI, "font" ); | |
| 98 | public static final Key KEY_UI_FONT_EDITOR = key( KEY_UI_FONT, "editor" ); | |
| 99 | public static final Key KEY_UI_FONT_EDITOR_NAME = key( KEY_UI_FONT_EDITOR, "name" ); | |
| 100 | public static final Key KEY_UI_FONT_EDITOR_SIZE = key( KEY_UI_FONT_EDITOR, "size" ); | |
| 101 | public static final Key KEY_UI_FONT_PREVIEW = key( KEY_UI_FONT, "preview" ); | |
| 102 | public static final Key KEY_UI_FONT_PREVIEW_NAME = key( KEY_UI_FONT_PREVIEW, "name" ); | |
| 103 | public static final Key KEY_UI_FONT_PREVIEW_SIZE = key( KEY_UI_FONT_PREVIEW, "size" ); | |
| 104 | public static final Key KEY_UI_FONT_PREVIEW_MONO = key( KEY_UI_FONT_PREVIEW, "mono" ); | |
| 105 | public static final Key KEY_UI_FONT_PREVIEW_MONO_NAME = key( KEY_UI_FONT_PREVIEW_MONO, "name" ); | |
| 106 | public static final Key KEY_UI_FONT_PREVIEW_MONO_SIZE = key( KEY_UI_FONT_PREVIEW_MONO, "size" ); | |
| 107 | ||
| 108 | public static final Key KEY_LANGUAGE = key( KEY_ROOT, "language" ); | |
| 109 | public static final Key KEY_LANG_LOCALE = key( KEY_LANGUAGE, "locale" ); | |
| 110 | ||
| 111 | public static final Key KEY_UI_WINDOW = key( KEY_UI, "window" ); | |
| 112 | public static final Key KEY_UI_WINDOW_X = key( KEY_UI_WINDOW, "x" ); | |
| 113 | public static final Key KEY_UI_WINDOW_Y = key( KEY_UI_WINDOW, "y" ); | |
| 114 | public static final Key KEY_UI_WINDOW_W = key( KEY_UI_WINDOW, "width" ); | |
| 115 | public static final Key KEY_UI_WINDOW_H = key( KEY_UI_WINDOW, "height" ); | |
| 116 | public static final Key KEY_UI_WINDOW_MAX = key( KEY_UI_WINDOW, "maximized" ); | |
| 117 | public static final Key KEY_UI_WINDOW_FULL = key( KEY_UI_WINDOW, "full" ); | |
| 118 | ||
| 119 | private final Map<Key, Property<?>> VALUES = Map.ofEntries( | |
| 120 | entry( KEY_META_VERSION, asStringProperty( getVersion() ) ), | |
| 121 | entry( KEY_META_NAME, asStringProperty( "default" ) ), | |
| 122 | ||
| 123 | entry( KEY_R_SCRIPT, asStringProperty( "" ) ), | |
| 124 | entry( KEY_R_DIR, asFileProperty( USER_DIRECTORY ) ), | |
| 125 | entry( KEY_R_DELIM_BEGAN, asStringProperty( R_DELIM_BEGAN_DEFAULT ) ), | |
| 126 | entry( KEY_R_DELIM_ENDED, asStringProperty( R_DELIM_ENDED_DEFAULT ) ), | |
| 127 | ||
| 128 | entry( KEY_IMAGES_DIR, asFileProperty( USER_DIRECTORY ) ), | |
| 129 | entry( KEY_IMAGES_ORDER, asStringProperty( PERSIST_IMAGES_DEFAULT ) ), | |
| 130 | ||
| 131 | entry( KEY_DEF_PATH, asFileProperty( DEFINITION_DEFAULT ) ), | |
| 132 | entry( KEY_DEF_DELIM_BEGAN, asStringProperty( DEF_DELIM_BEGAN_DEFAULT ) ), | |
| 133 | entry( KEY_DEF_DELIM_ENDED, asStringProperty( DEF_DELIM_ENDED_DEFAULT ) ), | |
| 134 | ||
| 135 | entry( KEY_UI_RECENT_DIR, asFileProperty( USER_DIRECTORY ) ), | |
| 136 | entry( KEY_UI_RECENT_DOCUMENT, asFileProperty( DOCUMENT_DEFAULT ) ), | |
| 137 | entry( KEY_UI_RECENT_DEFINITION, asFileProperty( DEFINITION_DEFAULT ) ), | |
| 138 | ||
| 139 | entry( KEY_LANG_LOCALE, asLocaleProperty( LOCALE_DEFAULT ) ), | |
| 140 | entry( KEY_UI_FONT_EDITOR_NAME, asStringProperty( FONT_NAME_EDITOR_DEFAULT ) ), | |
| 141 | entry( KEY_UI_FONT_EDITOR_SIZE, asDoubleProperty( FONT_SIZE_EDITOR_DEFAULT ) ), | |
| 142 | entry( KEY_UI_FONT_PREVIEW_NAME, asStringProperty( FONT_NAME_PREVIEW_DEFAULT ) ), | |
| 143 | entry( KEY_UI_FONT_PREVIEW_SIZE, asDoubleProperty( FONT_SIZE_PREVIEW_DEFAULT ) ), | |
| 144 | entry( KEY_UI_FONT_PREVIEW_MONO_NAME, asStringProperty( FONT_NAME_PREVIEW_MONO_NAME_DEFAULT ) ), | |
| 145 | entry( KEY_UI_FONT_PREVIEW_MONO_SIZE, asDoubleProperty( FONT_SIZE_PREVIEW_MONO_SIZE_DEFAULT ) ), | |
| 146 | ||
| 147 | entry( KEY_UI_WINDOW_X, asDoubleProperty( WINDOW_X_DEFAULT ) ), | |
| 148 | entry( KEY_UI_WINDOW_Y, asDoubleProperty( WINDOW_Y_DEFAULT ) ), | |
| 149 | entry( KEY_UI_WINDOW_W, asDoubleProperty( WINDOW_W_DEFAULT ) ), | |
| 150 | entry( KEY_UI_WINDOW_H, asDoubleProperty( WINDOW_H_DEFAULT ) ), | |
| 151 | entry( KEY_UI_WINDOW_MAX, asBooleanProperty() ), | |
| 152 | entry( KEY_UI_WINDOW_FULL, asBooleanProperty() ) | |
| 153 | ); | |
| 154 | //@formatter:on | |
| 155 | ||
| 156 | private StringProperty asStringProperty( final String defaultValue ) { | |
| 157 | return new SimpleStringProperty( defaultValue ); | |
| 158 | } | |
| 159 | ||
| 160 | private DoubleProperty asDoubleProperty( final double defaultValue ) { | |
| 161 | return new SimpleDoubleProperty( defaultValue ); | |
| 162 | } | |
| 163 | ||
| 164 | private BooleanProperty asBooleanProperty() { | |
| 165 | return new SimpleBooleanProperty(); | |
| 166 | } | |
| 167 | ||
| 168 | private FileProperty asFileProperty( final File defaultValue ) { | |
| 169 | return new FileProperty( defaultValue ); | |
| 170 | } | |
| 171 | ||
| 172 | @SuppressWarnings( "SameParameterValue" ) | |
| 173 | private LocaleProperty asLocaleProperty( final Locale defaultValue ) { | |
| 174 | return new LocaleProperty( defaultValue ); | |
| 175 | } | |
| 176 | ||
| 177 | /** | |
| 178 | * Helps instantiate {@link Property} instances for XML configuration items. | |
| 179 | */ | |
| 180 | private static final Map<Class<?>, Function<String, Object>> UNMARSHALL = | |
| 181 | Map.of( | |
| 182 | LocaleProperty.class, LocaleProperty::parseLocale, | |
| 183 | SimpleBooleanProperty.class, Boolean::parseBoolean, | |
| 184 | SimpleDoubleProperty.class, Double::parseDouble, | |
| 185 | SimpleFloatProperty.class, Float::parseFloat, | |
| 186 | FileProperty.class, File::new | |
| 187 | ); | |
| 188 | ||
| 189 | private static final Map<Class<?>, Function<String, Object>> MARSHALL = | |
| 190 | Map.of( | |
| 191 | LocaleProperty.class, LocaleProperty::toLanguageTag | |
| 192 | ); | |
| 193 | ||
| 194 | private final Map<Key, SetProperty<?>> SETS = Map.ofEntries( | |
| 195 | entry( | |
| 196 | KEY_UI_FILES_PATH, | |
| 197 | new SimpleSetProperty<>( observableSet( new HashSet<>() ) ) | |
| 198 | ) | |
| 199 | ); | |
| 200 | ||
| 201 | /** | |
| 202 | * Creates a new {@link Workspace} that will attempt to load a configuration | |
| 203 | * file. If the configuration file cannot be loaded, the workspace settings | |
| 204 | * will return default values. This allows unit tests to provide an instance | |
| 205 | * of {@link Workspace} when necessary without encountering failures. | |
| 206 | */ | |
| 207 | public Workspace() { | |
| 208 | load( FILE_PREFERENCES ); | |
| 209 | } | |
| 210 | ||
| 211 | /** | |
| 212 | * Creates a new {@link Workspace} that will attempt to load the given | |
| 213 | * configuration file. | |
| 214 | * | |
| 215 | * @param filename The file to load. | |
| 216 | */ | |
| 217 | public Workspace( final String filename ) { | |
| 218 | load( filename ); | |
| 219 | } | |
| 220 | ||
| 221 | /** | |
| 222 | * Creates an instance of {@link ObservableList} that is based on a | |
| 223 | * modifiable observable array list for the given items. | |
| 224 | * | |
| 225 | * @param items The items to wrap in an observable list. | |
| 226 | * @param <E> The type of items to add to the list. | |
| 227 | * @return An observable property that can have its contents modified. | |
| 228 | */ | |
| 229 | public static <E> ObservableList<E> listProperty( final Set<E> items ) { | |
| 230 | return new SimpleListProperty<>( observableArrayList( items ) ); | |
| 231 | } | |
| 232 | ||
| 233 | /** | |
| 234 | * Returns a value that represents a setting in the application that the user | |
| 235 | * may configure, either directly or indirectly. | |
| 236 | * | |
| 237 | * @param key The reference to the users' preference stored in deference | |
| 238 | * of app reëntrance. | |
| 239 | * @return An observable property to be persisted. | |
| 240 | */ | |
| 241 | @SuppressWarnings( "unchecked" ) | |
| 242 | public <T, U extends Property<T>> U valuesProperty( final Key key ) { | |
| 243 | // The type that goes into the map must come out. | |
| 244 | return (U) VALUES.get( key ); | |
| 245 | } | |
| 246 | ||
| 247 | /** | |
| 248 | * Returns a list of values that represent a setting in the application that | |
| 249 | * the user may configure, either directly or indirectly. The property | |
| 250 | * returned is backed by a mutable {@link Set}. | |
| 251 | * | |
| 252 | * @param key The {@link Key} associated with a preference value. | |
| 253 | * @return An observable property to be persisted. | |
| 254 | */ | |
| 255 | @SuppressWarnings( "unchecked" ) | |
| 256 | public <T> SetProperty<T> setsProperty( final Key key ) { | |
| 257 | // The type that goes into the map must come out. | |
| 258 | return (SetProperty<T>) SETS.get( key ); | |
| 259 | } | |
| 260 | ||
| 261 | /** | |
| 262 | * Returns the {@link Boolean} preference value associated with the given | |
| 263 | * {@link Key}. The caller must be sure that the given {@link Key} is | |
| 264 | * associated with a value that matches the return type. | |
| 265 | * | |
| 266 | * @param key The {@link Key} associated with a preference value. | |
| 267 | * @return The value associated with the given {@link Key}. | |
| 268 | */ | |
| 269 | public boolean toBoolean( final Key key ) { | |
| 270 | return (Boolean) valuesProperty( key ).getValue(); | |
| 271 | } | |
| 272 | ||
| 273 | /** | |
| 274 | * Returns the {@link Double} preference value associated with the given | |
| 275 | * {@link Key}. The caller must be sure that the given {@link Key} is | |
| 276 | * associated with a value that matches the return type. | |
| 277 | * | |
| 278 | * @param key The {@link Key} associated with a preference value. | |
| 279 | * @return The value associated with the given {@link Key}. | |
| 280 | */ | |
| 281 | public double toDouble( final Key key ) { | |
| 282 | return (Double) valuesProperty( key ).getValue(); | |
| 283 | } | |
| 284 | ||
| 285 | public File toFile( final Key key ) { | |
| 286 | return fileProperty( key ).get(); | |
| 287 | } | |
| 288 | ||
| 289 | public String toString( final Key key ) { | |
| 290 | return stringProperty( key ).get(); | |
| 291 | } | |
| 292 | ||
| 293 | public Tokens toTokens( final Key began, final Key ended ) { | |
| 294 | return new Tokens( stringProperty( began ), stringProperty( ended ) ); | |
| 295 | } | |
| 296 | ||
| 297 | @SuppressWarnings( "SameParameterValue" ) | |
| 298 | public DoubleProperty doubleProperty( final Key key ) { | |
| 299 | return valuesProperty( key ); | |
| 300 | } | |
| 301 | ||
| 302 | /** | |
| 303 | * Returns the {@link File} {@link Property} associated with the given | |
| 304 | * {@link Key} from the internal list of preference values. The caller | |
| 305 | * must be sure that the given {@link Key} is associated with a {@link File} | |
| 306 | * {@link Property}. | |
| 307 | * | |
| 308 | * @param key The {@link Key} associated with a preference value. | |
| 309 | * @return The value associated with the given {@link Key}. | |
| 310 | */ | |
| 311 | public ObjectProperty<File> fileProperty( final Key key ) { | |
| 312 | return valuesProperty( key ); | |
| 313 | } | |
| 314 | ||
| 315 | public LocaleProperty localeProperty( final Key key ) { | |
| 316 | return valuesProperty( key ); | |
| 317 | } | |
| 318 | ||
| 319 | /** | |
| 320 | * Returns the language locale setting for the {@link #KEY_LANG_LOCALE} key. | |
| 321 | * | |
| 322 | * @return The user's current locale setting. | |
| 323 | */ | |
| 324 | public Locale getLocale() { | |
| 325 | return localeProperty( KEY_LANG_LOCALE ).toLocale(); | |
| 326 | } | |
| 327 | ||
| 328 | public StringProperty stringProperty( final Key key ) { | |
| 329 | return valuesProperty( key ); | |
| 330 | } | |
| 331 | ||
| 332 | public void loadValueKeys( final Consumer<Key> consumer ) { | |
| 333 | VALUES.keySet().forEach( consumer ); | |
| 334 | } | |
| 335 | ||
| 336 | public void loadSetKeys( final Consumer<Key> consumer ) { | |
| 337 | SETS.keySet().forEach( consumer ); | |
| 338 | } | |
| 339 | ||
| 340 | /** | |
| 341 | * Calls the given consumer for all single-value keys. For lists, see | |
| 342 | * {@link #saveSets(BiConsumer)}. | |
| 343 | * | |
| 344 | * @param consumer Called to accept each preference key value. | |
| 345 | */ | |
| 346 | public void saveValues( final BiConsumer<Key, Property<?>> consumer ) { | |
| 347 | VALUES.forEach( consumer ); | |
| 348 | } | |
| 349 | ||
| 350 | /** | |
| 351 | * Calls the given consumer for all multi-value keys. For single items, see | |
| 352 | * {@link #saveValues(BiConsumer)}. Callers are responsible for iterating | |
| 353 | * over the list of items retrieved through this method. | |
| 354 | * | |
| 355 | * @param consumer Called to accept each preference key list. | |
| 356 | */ | |
| 357 | public void saveSets( final BiConsumer<Key, SetProperty<?>> consumer ) { | |
| 358 | SETS.forEach( consumer ); | |
| 359 | } | |
| 360 | ||
| 361 | /** | |
| 362 | * Delegates to {@link #listen(Key, ReadOnlyProperty, BooleanSupplier)}, | |
| 363 | * providing a value of {@code true} for the {@link BooleanSupplier} to | |
| 364 | * indicate the property changes always take effect. | |
| 365 | * | |
| 366 | * @param key The value to bind to the internal key property. | |
| 367 | * @param property The external property value that sets the internal value. | |
| 368 | */ | |
| 369 | public <T> void listen( final Key key, final ReadOnlyProperty<T> property ) { | |
| 370 | listen( key, property, () -> true ); | |
| 371 | } | |
| 372 | ||
| 373 | /** | |
| 374 | * Binds a read-only property to a value in the preferences. This allows | |
| 375 | * user interface properties to change and the preferences will be | |
| 376 | * synchronized automatically. | |
| 377 | * <p> | |
| 378 | * This calls {@link Platform#runLater(Runnable)} to ensure that all pending | |
| 379 | * application window states are finished before assessing whether property | |
| 380 | * changes should be applied. Without this, exiting the application while the | |
| 381 | * window is maximized would persist the window's maximum dimensions, | |
| 382 | * preventing restoration to its prior, non-maximum size. | |
| 383 | * </p> | |
| 384 | * | |
| 385 | * @param key The value to bind to the internal key property. | |
| 386 | * @param property The external property value that sets the internal value. | |
| 387 | * @param enabled Indicates whether property changes should be applied. | |
| 388 | */ | |
| 389 | public <T> void listen( | |
| 390 | final Key key, | |
| 391 | final ReadOnlyProperty<T> property, | |
| 392 | final BooleanSupplier enabled ) { | |
| 393 | property.addListener( | |
| 394 | ( c, o, n ) -> runLater( () -> { | |
| 395 | if( enabled.getAsBoolean() ) { | |
| 396 | valuesProperty( key ).setValue( n ); | |
| 397 | } | |
| 398 | } ) | |
| 399 | ); | |
| 400 | } | |
| 401 | ||
| 402 | /** | |
| 403 | * Saves the current workspace. | |
| 404 | */ | |
| 405 | public void save() { | |
| 406 | try { | |
| 407 | final var config = new XMLConfiguration(); | |
| 408 | ||
| 409 | // The root config key can only be set for an empty configuration file. | |
| 410 | config.setRootElementName( APP_TITLE_LOWERCASE ); | |
| 411 | valuesProperty( KEY_META_VERSION ).setValue( getVersion() ); | |
| 412 | ||
| 413 | saveValues( ( key, property ) -> | |
| 414 | config.setProperty( key.toString(), marshall( property ) ) | |
| 415 | ); | |
| 416 | ||
| 417 | saveSets( ( key, set ) -> { | |
| 418 | final var keyName = key.toString(); | |
| 419 | set.forEach( ( value ) -> config.addProperty( keyName, value ) ); | |
| 420 | } ); | |
| 421 | new FileHandler( config ).save( FILE_PREFERENCES ); | |
| 422 | } catch( final Exception ex ) { | |
| 423 | clue( ex ); | |
| 424 | } | |
| 425 | } | |
| 426 | ||
| 427 | /** | |
| 428 | * Attempts to load the {@link Constants#FILE_PREFERENCES} configuration file. | |
| 429 | * If not found, this will fall back to an empty configuration file, leaving | |
| 430 | * the application to fill in default values. | |
| 431 | * | |
| 432 | * @param filename The file containing user preferences to load. | |
| 433 | */ | |
| 434 | private void load( final String filename ) { | |
| 435 | try { | |
| 436 | final var config = new Configurations().xml( filename ); | |
| 425 | 437 | |
| 426 | 438 | loadValueKeys( ( key ) -> { |
| 16 | 16 | * </p> |
| 17 | 17 | */ |
| 18 | public class XmlStorageHandler implements StorageHandler { | |
| 18 | public final class XmlStorageHandler implements StorageHandler { | |
| 19 | 19 | @Override |
| 20 | 20 | public void saveSelectedCategory( final String breadcrumb ) { } |
| 41 | 41 | * the HTML document prior to displaying it. |
| 42 | 42 | */ |
| 43 | public class ChainedReplacedElementFactory extends ReplacedElementAdapter { | |
| 43 | public final class ChainedReplacedElementFactory extends ReplacedElementAdapter { | |
| 44 | 44 | /** |
| 45 | 45 | * Retain insertion order so that client classes can control the order that |
| 23 | 23 | * builders, and implementations. |
| 24 | 24 | */ |
| 25 | class DomConverter extends W3CDom { | |
| 25 | final class DomConverter extends W3CDom { | |
| 26 | 26 | /** |
| 27 | 27 | * Retain insertion order using an instance of {@link LinkedHashMap} so |
| 28 | 28 | * Responsible for configuring FlyingSaucer's {@link XHTMLPanel}. |
| 29 | 29 | */ |
| 30 | public class HtmlPanel extends XHTMLPanel { | |
| 30 | public final class HtmlPanel extends XHTMLPanel { | |
| 31 | 31 | |
| 32 | 32 | /** |
| 13 | 13 | * Responsible for rendering formulas as scalable vector graphics (SVG). |
| 14 | 14 | */ |
| 15 | public class MathRenderer { | |
| 15 | public final class MathRenderer { | |
| 16 | 16 | |
| 17 | 17 | /** |
| 13 | 13 | */ |
| 14 | 14 | @SuppressWarnings("rawtypes") |
| 15 | public class RenderingSettings { | |
| 15 | public final class RenderingSettings { | |
| 16 | 16 | |
| 17 | 17 | /** |
| 36 | 36 | * Responsible for converting SVG images into rasterized PNG images. |
| 37 | 37 | */ |
| 38 | public class SvgRasterizer { | |
| 38 | public final class SvgRasterizer { | |
| 39 | 39 | private static final SAXSVGDocumentFactory FACTORY_DOM = |
| 40 | 40 | new SAXSVGDocumentFactory( getXMLParserClassName() ); |
| 14 | 14 | import java.awt.image.BufferedImage; |
| 15 | 15 | import java.net.URI; |
| 16 | import java.nio.file.Paths; | |
| 16 | import java.nio.file.Path; | |
| 17 | 17 | |
| 18 | 18 | import static com.keenwrite.StatusNotifier.clue; |
| ... | ||
| 28 | 28 | * a document to transform them into rasterized versions. |
| 29 | 29 | */ |
| 30 | public class SvgReplacedElementFactory extends ReplacedElementAdapter { | |
| 30 | public final class SvgReplacedElementFactory extends ReplacedElementAdapter { | |
| 31 | 31 | |
| 32 | 32 | public static final String HTML_IMAGE = "img"; |
| ... | ||
| 69 | 69 | else if( isSvg( MediaType.valueFrom( source ) ) ) { |
| 70 | 70 | // Attempt to rasterize based on file name. |
| 71 | final var base = new URI( getBaseUri( e ) ).getPath(); | |
| 72 | uri = Paths.get( base, source ).toUri(); | |
| 71 | final var path = Path.of( new URI( source ).getPath() ); | |
| 72 | ||
| 73 | if( path.isAbsolute() ) { | |
| 74 | uri = path.toUri(); | |
| 75 | } | |
| 76 | else { | |
| 77 | final var base = new URI( getBaseUri( e ) ).getPath(); | |
| 78 | uri = Path.of( base, source ).toUri(); | |
| 79 | } | |
| 73 | 80 | } |
| 74 | 81 | |
| 11 | 11 | * chain. |
| 12 | 12 | */ |
| 13 | public class HtmlPreviewProcessor extends ExecutorProcessor<String> { | |
| 13 | public final class HtmlPreviewProcessor extends ExecutorProcessor<String> { | |
| 14 | 14 | |
| 15 | 15 | /** |
| 6 | 6 | * end of a processing chain when no more processing is required. |
| 7 | 7 | */ |
| 8 | public class IdentityProcessor extends ExecutorProcessor<String> { | |
| 8 | public final class IdentityProcessor extends ExecutorProcessor<String> { | |
| 9 | 9 | public static final IdentityProcessor IDENTITY = new IdentityProcessor(); |
| 10 | 10 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors; | |
| 3 | ||
| 4 | import com.keenwrite.preferences.Workspace; | |
| 5 | import com.keenwrite.processors.markdown.extensions.r.ROutputProcessor; | |
| 6 | import com.keenwrite.util.BoundedCache; | |
| 7 | import javafx.beans.property.Property; | |
| 8 | ||
| 9 | import javax.script.ScriptEngine; | |
| 10 | import javax.script.ScriptEngineManager; | |
| 11 | import java.io.File; | |
| 12 | import java.nio.file.Path; | |
| 13 | import java.util.Map; | |
| 14 | import java.util.concurrent.atomic.AtomicBoolean; | |
| 15 | ||
| 16 | import static com.keenwrite.Constants.STATUS_PARSE_ERROR; | |
| 17 | import static com.keenwrite.Messages.get; | |
| 18 | import static com.keenwrite.StatusNotifier.clue; | |
| 19 | import static com.keenwrite.preferences.Workspace.*; | |
| 20 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; | |
| 21 | import static com.keenwrite.sigils.RSigilOperator.PREFIX; | |
| 22 | import static com.keenwrite.sigils.RSigilOperator.SUFFIX; | |
| 23 | import static java.lang.Math.max; | |
| 24 | import static java.lang.Math.min; | |
| 25 | ||
| 26 | /** | |
| 27 | * Transforms a document containing R statements into Markdown. | |
| 28 | */ | |
| 29 | public final class InlineRProcessor extends DefinitionProcessor { | |
| 30 | private final Processor<String> mPostProcessor = new ROutputProcessor(); | |
| 31 | ||
| 32 | /** | |
| 33 | * Where to put document inline evaluated R expressions, constrained to | |
| 34 | * avoid running out of memory. | |
| 35 | */ | |
| 36 | private final Map<String, String> mEvalCache = | |
| 37 | new BoundedCache<>( 512 ); | |
| 38 | ||
| 39 | private static final ScriptEngine ENGINE = | |
| 40 | (new ScriptEngineManager()).getEngineByName( "Renjin" ); | |
| 41 | ||
| 42 | private static final int PREFIX_LENGTH = PREFIX.length(); | |
| 43 | ||
| 44 | private final AtomicBoolean mDirty = new AtomicBoolean( false ); | |
| 45 | ||
| 46 | private final Workspace mWorkspace; | |
| 47 | ||
| 48 | /** | |
| 49 | * Constructs a processor capable of evaluating R statements. | |
| 50 | * | |
| 51 | * @param successor Subsequent link in the processing chain. | |
| 52 | * @param context Contains resolved definitions map. | |
| 53 | */ | |
| 54 | public InlineRProcessor( | |
| 55 | final Processor<String> successor, | |
| 56 | final ProcessorContext context ) { | |
| 57 | super( successor, context ); | |
| 58 | ||
| 59 | mWorkspace = context.getWorkspace(); | |
| 60 | ||
| 61 | bootstrapScriptProperty().addListener( | |
| 62 | ( __, oldScript, newScript ) -> setDirty( true ) ); | |
| 63 | workingDirectoryProperty().addListener( | |
| 64 | ( __, oldScript, newScript ) -> setDirty( true ) ); | |
| 65 | ||
| 66 | // TODO: Watch the "R" property keys in the workspace, directly. | |
| 67 | ||
| 68 | // If the user saves the preferences, make sure that any R-related settings | |
| 69 | // changes are applied. | |
| 70 | // getWorkspace().addSaveEventHandler( ( handler ) -> { | |
| 71 | // if( isDirty() ) { | |
| 72 | // init(); | |
| 73 | // setDirty( false ); | |
| 74 | // } | |
| 75 | // } ); | |
| 76 | ||
| 77 | init(); | |
| 78 | } | |
| 79 | ||
| 80 | /** | |
| 81 | * Initialises the R code so that R can find imported libraries. Note that | |
| 82 | * any existing R functionality will not be overwritten if this method is | |
| 83 | * called multiple times. | |
| 84 | * | |
| 85 | * @return {@code true} if initialization completed and all variables were | |
| 86 | * replaced; {@code false} if any variables remain. | |
| 87 | */ | |
| 88 | public boolean init() { | |
| 89 | final var bootstrap = getBootstrapScript(); | |
| 90 | ||
| 91 | if( !bootstrap.isBlank() ) { | |
| 92 | final var wd = getWorkingDirectory(); | |
| 93 | final var dir = wd.toString().replace( '\\', '/' ); | |
| 94 | final var map = getDefinitions(); | |
| 95 | final var defBegan = mWorkspace.toString( KEY_DEF_DELIM_BEGAN ); | |
| 96 | final var defEnded = mWorkspace.toString( KEY_DEF_DELIM_ENDED ); | |
| 97 | ||
| 98 | map.put( defBegan + "application.r.working.directory" + defEnded, dir ); | |
| 99 | ||
| 100 | final var replaced = replace( bootstrap, map ); | |
| 101 | final var bIndex = replaced.indexOf( defBegan ); | |
| 102 | ||
| 103 | // | |
| 104 | if( bIndex >= 0 ) { | |
| 105 | var eIndex = replaced.indexOf( defEnded ); | |
| 106 | eIndex = (eIndex == -1) ? replaced.length() - 1 : max( bIndex, eIndex ); | |
| 107 | ||
| 108 | final var def = replaced.substring( bIndex, eIndex ); | |
| 109 | clue( "Main.status.error.bootstrap.eval", def ); | |
| 110 | ||
| 111 | return false; | |
| 112 | } | |
| 113 | else { | |
| 114 | eval( replaced ); | |
| 115 | } | |
| 116 | } | |
| 117 | ||
| 118 | return true; | |
| 119 | } | |
| 120 | ||
| 121 | /** | |
| 122 | * Empties the cache. | |
| 123 | */ | |
| 124 | public void clear() { | |
| 125 | mEvalCache.clear(); | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * Sets the dirty flag to indicate that the bootstrap script or working | |
| 130 | * directory has been modified. Upon saving the preferences, if this flag | |
| 131 | * is true, then {@link #init()} will be called to reload the R environment. | |
| 132 | * | |
| 133 | * @param dirty Set to true to reload changes upon closing preferences. | |
| 134 | */ | |
| 135 | private void setDirty( final boolean dirty ) { | |
| 136 | mDirty.set( dirty ); | |
| 137 | } | |
| 138 | ||
| 139 | /** | |
| 140 | * Answers whether R-related settings have been modified. | |
| 141 | * | |
| 142 | * @return {@code true} when the settings have changed. | |
| 143 | */ | |
| 144 | private boolean isDirty() { | |
| 145 | return mDirty.get(); | |
| 146 | } | |
| 147 | ||
| 148 | /** | |
| 149 | * Evaluates all R statements in the source document and inserts the | |
| 150 | * calculated value into the generated document. | |
| 151 | * | |
| 152 | * @param text The document text that includes variables that should be | |
| 153 | * replaced with values when rendered as HTML. | |
| 154 | * @return The generated document with output from all R statements | |
| 155 | * substituted with value returned from their execution. | |
| 156 | */ | |
| 157 | @Override | |
| 158 | public String apply( final String text ) { | |
| 159 | final int length = text.length(); | |
| 160 | ||
| 161 | // The * 2 is a wild guess at the ratio of R statements to the length | |
| 162 | // of text produced by those statements. | |
| 163 | final StringBuilder sb = new StringBuilder( length * 2 ); | |
| 164 | ||
| 165 | int prevIndex = 0; | |
| 166 | int currIndex = text.indexOf( PREFIX ); | |
| 167 | ||
| 168 | while( currIndex >= 0 ) { | |
| 169 | // Copy everything up to, but not including, the opening token. | |
| 170 | sb.append( text, prevIndex, currIndex ); | |
| 171 | ||
| 172 | // Jump to the start of the R statement. | |
| 173 | prevIndex = currIndex + PREFIX_LENGTH; | |
| 174 | ||
| 175 | // Find the closing token, without indexing past the text boundary. | |
| 176 | currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) ); | |
| 177 | ||
| 178 | // Only evaluate inline R statements that have end delimiters. | |
| 179 | if( currIndex > 1 ) { | |
| 180 | // Extract the inline R statement to be evaluated. | |
| 181 | final var r = text.substring( prevIndex, currIndex ); | |
| 182 | ||
| 183 | // Pass the R statement into the R engine for evaluation. | |
| 184 | try { | |
| 185 | // Append the string representation of the result into the text. | |
| 186 | sb.append( evalCached( r ) ); | |
| 187 | } catch( final Exception ex ) { | |
| 188 | // Inform the user that there was a problem. | |
| 189 | clue( STATUS_PARSE_ERROR, ex.getMessage(), currIndex ); | |
| 190 | ||
| 191 | // If the string couldn't be parsed using R, append the statement | |
| 192 | // that failed to parse, instead of its evaluated value. | |
| 193 | sb.append( PREFIX ).append( r ).append( SUFFIX ); | |
| 194 | } | |
| 195 | ||
| 196 | // Retain the R statement's ending position in the text. | |
| 197 | prevIndex = currIndex + 1; | |
| 198 | } | |
| 199 | ||
| 200 | // Find the start of the next inline R statement. | |
| 201 | currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) ); | |
| 202 | } | |
| 203 | ||
| 204 | // Copy from the previous index to the end of the string. | |
| 205 | return sb.append( text.substring( min( prevIndex, length ) ) ).toString(); | |
| 206 | } | |
| 207 | ||
| 208 | /** | |
| 209 | * Look up an R expression from the cache then return the resulting object. | |
| 210 | * If the R expression hasn't been cached, it'll first be evaluated. | |
| 211 | * | |
| 212 | * @param r The expression to evaluate. | |
| 213 | * @return The object resulting from the evaluation. | |
| 214 | */ | |
| 215 | private String evalCached( final String r ) { | |
| 216 | return mEvalCache.computeIfAbsent( r, __ -> evalHtml( r ) ); | |
| 217 | } | |
| 218 | ||
| 219 | /** | |
| 220 | * Converts the given string to HTML, trimming new lines, and inlining | |
| 221 | * the text if it is a paragraph. Otherwise, the resulting HTML is most likely | |
| 222 | * complex (e.g., a Markdown table) and should be rendered as its HTML | |
| 223 | * equivalent. | |
| 224 | * | |
| 225 | * @param r The R expression to evaluate then convert to HTML. | |
| 226 | * @return The result from the R expression as an HTML element. | |
| 227 | */ | |
| 228 | private String evalHtml( final String r ) { | |
| 229 | return mPostProcessor.apply( eval( r ) ); | |
| 230 | } | |
| 231 | ||
| 232 | /** | |
| 233 | * Evaluate an R expression and return the resulting object. | |
| 234 | * | |
| 235 | * @param r The expression to evaluate. | |
| 236 | * @return The object resulting from the evaluation. | |
| 237 | */ | |
| 238 | private String eval( final String r ) { | |
| 239 | try { | |
| 240 | return ENGINE.eval( r ).toString(); | |
| 241 | } catch( final Exception ex ) { | |
| 242 | final var expr = r.substring( 0, min( r.length(), 50 ) ); | |
| 243 | clue( get( "Main.status.error.r", expr, ex.getMessage() ), ex ); | |
| 244 | return ""; | |
| 245 | } | |
| 246 | } | |
| 247 | ||
| 248 | /** | |
| 249 | * Return the given path if not {@code null}, otherwise return the path to | |
| 250 | * the user's directory. | |
| 251 | * | |
| 252 | * @return A non-null path. | |
| 253 | */ | |
| 254 | private Path getWorkingDirectory() { | |
| 255 | return workingDirectoryProperty().getValue().toPath(); | |
| 256 | } | |
| 257 | ||
| 258 | private Property<File> workingDirectoryProperty() { | |
| 259 | return getWorkspace().fileProperty( KEY_R_DIR ); | |
| 260 | } | |
| 261 | ||
| 262 | /** | |
| 263 | * Loads the R init script from the application's persisted preferences. | |
| 264 | * | |
| 265 | * @return A non-null string, possibly empty. | |
| 266 | */ | |
| 267 | private String getBootstrapScript() { | |
| 268 | return bootstrapScriptProperty().getValue(); | |
| 269 | } | |
| 270 | ||
| 271 | private Property<String> bootstrapScriptProperty() { | |
| 272 | return getWorkspace().valuesProperty( KEY_R_SCRIPT ); | |
| 273 | } | |
| 274 | ||
| 275 | private Workspace getWorkspace() { | |
| 276 | return mWorkspace; | |
| 277 | } | |
| 278 | } | |
| 279 | 1 |
| 7 | 7 | * element. |
| 8 | 8 | */ |
| 9 | public class PreformattedProcessor extends ExecutorProcessor<String> { | |
| 9 | public final class PreformattedProcessor extends ExecutorProcessor<String> { | |
| 10 | 10 | |
| 11 | 11 | /** |
| 18 | 18 | * Provides a context for configuring a chain of {@link Processor} instances. |
| 19 | 19 | */ |
| 20 | public class ProcessorContext { | |
| 20 | public final class ProcessorContext { | |
| 21 | 21 | private final HtmlPreview mHtmlPreview; |
| 22 | 22 | private final Map<String, String> mResolvedMap; |
| 23 | private final Path mPath; | |
| 23 | private final Path mDocumentPath; | |
| 24 | 24 | private final Caret mCaret; |
| 25 | 25 | private final ExportFormat mExportFormat; |
| ... | ||
| 34 | 34 | * @param htmlPreview Where to display the final (HTML) output. |
| 35 | 35 | * @param resolvedMap Fully expanded interpolated strings. |
| 36 | * @param path Path to the document to process. | |
| 36 | * @param documentPath Path to the document to process. | |
| 37 | 37 | * @param caret Location of the caret in the edited document, which is |
| 38 | 38 | * used to synchronize the scrollbars. |
| 39 | 39 | * @param exportFormat Indicate configuration options for export format. |
| 40 | 40 | */ |
| 41 | 41 | public ProcessorContext( |
| 42 | 42 | final HtmlPreview htmlPreview, |
| 43 | 43 | final Map<String, String> resolvedMap, |
| 44 | final Path path, | |
| 44 | final Path documentPath, | |
| 45 | 45 | final Caret caret, |
| 46 | 46 | final ExportFormat exportFormat, |
| 47 | 47 | final Workspace workspace ) { |
| 48 | 48 | assert htmlPreview != null; |
| 49 | 49 | assert resolvedMap != null; |
| 50 | assert path != null; | |
| 50 | assert documentPath != null; | |
| 51 | 51 | assert caret != null; |
| 52 | 52 | assert exportFormat != null; |
| 53 | 53 | assert workspace != null; |
| 54 | 54 | |
| 55 | 55 | mHtmlPreview = htmlPreview; |
| 56 | 56 | mResolvedMap = resolvedMap; |
| 57 | mPath = path; | |
| 57 | mDocumentPath = documentPath; | |
| 58 | 58 | mCaret = caret; |
| 59 | 59 | mExportFormat = exportFormat; |
| ... | ||
| 106 | 106 | */ |
| 107 | 107 | public Path getBaseDir() { |
| 108 | final var path = getPath().toAbsolutePath().getParent(); | |
| 108 | final var path = getDocumentPath().toAbsolutePath().getParent(); | |
| 109 | 109 | return path == null ? DEFAULT_DIRECTORY : path; |
| 110 | 110 | } |
| 111 | 111 | |
| 112 | public Path getPath() { | |
| 113 | return mPath; | |
| 112 | public Path getDocumentPath() { | |
| 113 | return mDocumentPath; | |
| 114 | 114 | } |
| 115 | 115 | |
| 116 | 116 | FileType getFileType() { |
| 117 | return lookup( getPath() ); | |
| 117 | return lookup( getDocumentPath() ); | |
| 118 | 118 | } |
| 119 | 119 | |
| 12 | 12 | * interpolating, and rendering known file types. |
| 13 | 13 | */ |
| 14 | public class ProcessorFactory extends AbstractFileFactory { | |
| 14 | public final class ProcessorFactory extends AbstractFileFactory { | |
| 15 | 15 | |
| 16 | 16 | private final ProcessorContext mContext; |
| ... | ||
| 106 | 106 | private Processor<String> createRProcessor( |
| 107 | 107 | final Processor<String> successor ) { |
| 108 | final var irp = new InlineRProcessor( successor, getProcessorContext() ); | |
| 109 | final var rvp = new RVariableProcessor( irp, getProcessorContext() ); | |
| 110 | return MarkdownProcessor.create( rvp, getProcessorContext() ); | |
| 108 | return MarkdownProcessor.create( successor, getProcessorContext() ); | |
| 111 | 109 | } |
| 112 | 110 | |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors; | |
| 3 | ||
| 4 | import com.keenwrite.preferences.Workspace; | |
| 5 | import com.keenwrite.sigils.RSigilOperator; | |
| 6 | import com.keenwrite.sigils.SigilOperator; | |
| 7 | import com.keenwrite.sigils.YamlSigilOperator; | |
| 8 | ||
| 9 | import java.util.HashMap; | |
| 10 | import java.util.Map; | |
| 11 | ||
| 12 | import static com.keenwrite.preferences.Workspace.*; | |
| 13 | ||
| 14 | /** | |
| 15 | * Converts the keys of the resolved map from default form to R form, then | |
| 16 | * performs a substitution on the text. The default R variable syntax is | |
| 17 | * {@code v$tree$leaf}. | |
| 18 | */ | |
| 19 | public class RVariableProcessor extends DefinitionProcessor { | |
| 20 | ||
| 21 | private final SigilOperator mSigilOperator; | |
| 22 | ||
| 23 | public RVariableProcessor( | |
| 24 | final InlineRProcessor irp, final ProcessorContext context ) { | |
| 25 | super( irp, context ); | |
| 26 | mSigilOperator = createSigilOperator( context.getWorkspace() ); | |
| 27 | } | |
| 28 | ||
| 29 | /** | |
| 30 | * Returns the R-based version of the interpolated variable definitions. | |
| 31 | * | |
| 32 | * @return Variable names transmogrified from the default syntax to R syntax. | |
| 33 | */ | |
| 34 | @Override | |
| 35 | protected Map<String, String> getDefinitions() { | |
| 36 | return entoken( super.getDefinitions() ); | |
| 37 | } | |
| 38 | ||
| 39 | /** | |
| 40 | * Converts the given map from regular variables to R variables. | |
| 41 | * | |
| 42 | * @param map Map of variable names to values. | |
| 43 | * @return Map of R variables. | |
| 44 | */ | |
| 45 | private Map<String, String> entoken( final Map<String, String> map ) { | |
| 46 | final var rMap = new HashMap<String, String>( map.size() ); | |
| 47 | ||
| 48 | for( final var entry : map.entrySet() ) { | |
| 49 | final var key = entry.getKey(); | |
| 50 | rMap.put( mSigilOperator.entoken( key ), escape( map.get( key ) ) ); | |
| 51 | } | |
| 52 | ||
| 53 | return rMap; | |
| 54 | } | |
| 55 | ||
| 56 | private String escape( final String value ) { | |
| 57 | return '\'' + escape( value, '\'', "\\'" ) + '\''; | |
| 58 | } | |
| 59 | ||
| 60 | /** | |
| 61 | * TODO: Make generic method for replacing text. | |
| 62 | * | |
| 63 | * @param haystack Search this string for the needle, must not be null. | |
| 64 | * @param needle The character to find in the haystack. | |
| 65 | * @param thread Replace the needle with this text, if the needle is found. | |
| 66 | * @return The haystack with the all instances of needle replaced with thread. | |
| 67 | */ | |
| 68 | @SuppressWarnings("SameParameterValue") | |
| 69 | private String escape( | |
| 70 | final String haystack, final char needle, final String thread ) { | |
| 71 | int end = haystack.indexOf( needle ); | |
| 72 | ||
| 73 | if( end < 0 ) { | |
| 74 | return haystack; | |
| 75 | } | |
| 76 | ||
| 77 | final int length = haystack.length(); | |
| 78 | int start = 0; | |
| 79 | ||
| 80 | // Replace up to 32 occurrences before the string reallocates its buffer. | |
| 81 | final var sb = new StringBuilder( length + 32 ); | |
| 82 | ||
| 83 | while( end >= 0 ) { | |
| 84 | sb.append( haystack, start, end ).append( thread ); | |
| 85 | start = end + 1; | |
| 86 | end = haystack.indexOf( needle, start ); | |
| 87 | } | |
| 88 | ||
| 89 | return sb.append( haystack.substring( start ) ).toString(); | |
| 90 | } | |
| 91 | ||
| 92 | private SigilOperator createSigilOperator( final Workspace workspace ) { | |
| 93 | final var tokens = workspace.toTokens( | |
| 94 | KEY_R_DELIM_BEGAN, KEY_R_DELIM_ENDED ); | |
| 95 | final var antecedent = createDefinitionOperator( workspace ); | |
| 96 | return new RSigilOperator( tokens, antecedent ); | |
| 97 | } | |
| 98 | ||
| 99 | private SigilOperator createDefinitionOperator( | |
| 100 | final Workspace workspace ) { | |
| 101 | final var tokens = workspace.toTokens( | |
| 102 | KEY_DEF_DELIM_BEGAN, KEY_DEF_DELIM_ENDED ); | |
| 103 | return new YamlSigilOperator( tokens ); | |
| 104 | } | |
| 105 | } | |
| 106 | 1 |
| 33 | 33 | * </p> |
| 34 | 34 | */ |
| 35 | public class XmlProcessor extends ExecutorProcessor<String> | |
| 35 | public final class XmlProcessor extends ExecutorProcessor<String> | |
| 36 | 36 | implements ErrorListener { |
| 37 | 37 | |
| ... | ||
| 59 | 59 | final ProcessorContext context ) { |
| 60 | 60 | super( successor ); |
| 61 | mPath = context.getPath(); | |
| 61 | mPath = context.getDocumentPath(); | |
| 62 | 62 | |
| 63 | 63 | // Bubble problems up to the user interface, rather than standard error. |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.markdown; | |
| 3 | ||
| 4 | import com.keenwrite.processors.ExecutorProcessor; | |
| 5 | import com.keenwrite.processors.Processor; | |
| 6 | import com.keenwrite.processors.ProcessorContext; | |
| 7 | import com.keenwrite.processors.markdown.extensions.r.RExtension; | |
| 8 | import com.vladsch.flexmark.ext.definition.DefinitionExtension; | |
| 9 | import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension; | |
| 10 | import com.vladsch.flexmark.ext.superscript.SuperscriptExtension; | |
| 11 | import com.vladsch.flexmark.ext.tables.TablesExtension; | |
| 12 | import com.vladsch.flexmark.ext.typographic.TypographicExtension; | |
| 13 | import com.vladsch.flexmark.html.HtmlRenderer; | |
| 14 | import com.vladsch.flexmark.parser.Parser; | |
| 15 | import com.vladsch.flexmark.util.ast.IParse; | |
| 16 | import com.vladsch.flexmark.util.ast.IRender; | |
| 17 | import com.vladsch.flexmark.util.ast.Node; | |
| 18 | import com.vladsch.flexmark.util.misc.Extension; | |
| 19 | ||
| 20 | import java.util.ArrayList; | |
| 21 | import java.util.List; | |
| 22 | ||
| 23 | /** | |
| 24 | * Responsible for parsing and rendering Markdown into HTML. This is required | |
| 25 | * to break a circular dependency between the {@link MarkdownProcessor} and | |
| 26 | * {@link RExtension}. | |
| 27 | */ | |
| 28 | public class BaseMarkdownProcessor extends ExecutorProcessor<String> { | |
| 29 | ||
| 30 | private final IParse mParser; | |
| 31 | private final IRender mRenderer; | |
| 32 | ||
| 33 | public BaseMarkdownProcessor( | |
| 34 | final Processor<String> successor, final ProcessorContext context ) { | |
| 35 | super( successor ); | |
| 36 | ||
| 37 | final var extensions = new ArrayList<Extension>(); | |
| 38 | init( extensions, context ); | |
| 39 | ||
| 40 | mParser = Parser.builder().extensions( extensions ).build(); | |
| 41 | mRenderer = HtmlRenderer.builder().extensions( extensions ).build(); | |
| 42 | } | |
| 43 | ||
| 44 | /** | |
| 45 | * Instantiates a number of extensions to be applied when parsing. These | |
| 46 | * are typically typographic extensions that convert characters into | |
| 47 | * HTML entities. | |
| 48 | * | |
| 49 | * @param extensions A {@link List} of {@link Extension} instances that | |
| 50 | * change the {@link Parser}'s behaviour. | |
| 51 | * @param context The context that subclasses use to configure custom | |
| 52 | * extension behaviour. | |
| 53 | */ | |
| 54 | void init( | |
| 55 | final List<Extension> extensions, final ProcessorContext context ) { | |
| 56 | extensions.add( DefinitionExtension.create() ); | |
| 57 | extensions.add( StrikethroughSubscriptExtension.create() ); | |
| 58 | extensions.add( SuperscriptExtension.create() ); | |
| 59 | extensions.add( TablesExtension.create() ); | |
| 60 | extensions.add( TypographicExtension.create() ); | |
| 61 | } | |
| 62 | ||
| 63 | /** | |
| 64 | * Converts the given Markdown string into HTML, without the doctype, html, | |
| 65 | * head, and body tags. | |
| 66 | * | |
| 67 | * @param markdown The string to convert from Markdown to HTML. | |
| 68 | * @return The HTML representation of the Markdown document. | |
| 69 | */ | |
| 70 | @Override | |
| 71 | public String apply( final String markdown ) { | |
| 72 | return toHtml( parse( markdown ) ); | |
| 73 | } | |
| 74 | ||
| 75 | /** | |
| 76 | * Returns the AST in the form of a node for the given Markdown document. This | |
| 77 | * can be used, for example, to determine if a hyperlink exists inside of a | |
| 78 | * paragraph. | |
| 79 | * | |
| 80 | * @param markdown The Markdown to convert into an AST. | |
| 81 | * @return The Markdown AST for the given text (usually a paragraph). | |
| 82 | */ | |
| 83 | public Node toNode( final String markdown ) { | |
| 84 | return parse( markdown ); | |
| 85 | } | |
| 86 | ||
| 87 | /** | |
| 88 | * Returns the result of converting the given AST into an HTML string. | |
| 89 | * | |
| 90 | * @param node The AST {@link Node} to convert to an HTML string. | |
| 91 | * @return The given {@link Node} as an HTML string. | |
| 92 | */ | |
| 93 | public String toHtml( final Node node ) { | |
| 94 | return getRenderer().render( node ); | |
| 95 | } | |
| 96 | ||
| 97 | /** | |
| 98 | * Helper method to create an AST given some Markdown. | |
| 99 | * | |
| 100 | * @param markdown The Markdown to parse. | |
| 101 | * @return The root node of the Markdown tree. | |
| 102 | */ | |
| 103 | private Node parse( final String markdown ) { | |
| 104 | return getParser().parse( markdown ); | |
| 105 | } | |
| 106 | ||
| 107 | /** | |
| 108 | * Creates the Markdown document processor. | |
| 109 | * | |
| 110 | * @return An instance of {@link IParse} for building abstract syntax trees. | |
| 111 | */ | |
| 112 | private IParse getParser() { | |
| 113 | return mParser; | |
| 114 | } | |
| 115 | ||
| 116 | private IRender getRenderer() { | |
| 117 | return mRenderer; | |
| 118 | } | |
| 119 | } | |
| 1 | 120 |
| 3 | 3 | |
| 4 | 4 | import com.keenwrite.io.MediaType; |
| 5 | import com.keenwrite.processors.ExecutorProcessor; | |
| 6 | 5 | import com.keenwrite.processors.Processor; |
| 7 | 6 | import com.keenwrite.processors.ProcessorContext; |
| 8 | 7 | import com.keenwrite.processors.markdown.extensions.FencedBlockExtension; |
| 9 | 8 | import com.keenwrite.processors.markdown.extensions.ImageLinkExtension; |
| 10 | 9 | import com.keenwrite.processors.markdown.extensions.caret.CaretExtension; |
| 11 | 10 | import com.keenwrite.processors.markdown.extensions.r.RExtension; |
| 12 | 11 | import com.keenwrite.processors.markdown.extensions.tex.TeXExtension; |
| 13 | import com.vladsch.flexmark.ext.definition.DefinitionExtension; | |
| 14 | import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension; | |
| 15 | import com.vladsch.flexmark.ext.superscript.SuperscriptExtension; | |
| 16 | import com.vladsch.flexmark.ext.tables.TablesExtension; | |
| 17 | import com.vladsch.flexmark.ext.typographic.TypographicExtension; | |
| 18 | import com.vladsch.flexmark.html.HtmlRenderer; | |
| 19 | import com.vladsch.flexmark.parser.Parser; | |
| 20 | import com.vladsch.flexmark.util.ast.IParse; | |
| 21 | import com.vladsch.flexmark.util.ast.IRender; | |
| 22 | import com.vladsch.flexmark.util.ast.Node; | |
| 12 | import com.keenwrite.processors.r.RProcessor; | |
| 23 | 13 | import com.vladsch.flexmark.util.misc.Extension; |
| 24 | 14 | |
| 25 | import java.util.ArrayList; | |
| 26 | 15 | import java.util.List; |
| 27 | 16 | |
| 28 | 17 | import static com.keenwrite.io.MediaType.TEXT_R_MARKDOWN; |
| 29 | 18 | import static com.keenwrite.io.MediaType.TEXT_R_XML; |
| 30 | 19 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; |
| 31 | 20 | |
| 32 | 21 | /** |
| 33 | 22 | * Responsible for parsing a Markdown document and rendering it as HTML. |
| 34 | 23 | */ |
| 35 | public class MarkdownProcessor extends ExecutorProcessor<String> { | |
| 36 | ||
| 37 | private static final List<Extension> DEFAULT_EXTENSIONS = | |
| 38 | createDefaultExtensions(); | |
| 39 | ||
| 40 | private final IParse mParser; | |
| 41 | private final IRender mRenderer; | |
| 24 | public final class MarkdownProcessor extends BaseMarkdownProcessor { | |
| 42 | 25 | |
| 43 | 26 | private MarkdownProcessor( |
| 44 | final Processor<String> successor, | |
| 45 | final List<Extension> extensions ) { | |
| 46 | super( successor ); | |
| 47 | ||
| 48 | mParser = Parser.builder().extensions( extensions ).build(); | |
| 49 | mRenderer = HtmlRenderer.builder().extensions( extensions ).build(); | |
| 27 | final Processor<String> successor, final ProcessorContext context ) { | |
| 28 | super( successor, context ); | |
| 50 | 29 | } |
| 51 | 30 | |
| 52 | 31 | public static MarkdownProcessor create( final ProcessorContext context ) { |
| 53 | 32 | return create( IDENTITY, context ); |
| 54 | 33 | } |
| 55 | 34 | |
| 56 | 35 | public static MarkdownProcessor create( |
| 57 | 36 | final Processor<String> successor, final ProcessorContext context ) { |
| 58 | final var extensions = createExtensions( context ); | |
| 59 | return new MarkdownProcessor( successor, extensions ); | |
| 60 | } | |
| 61 | ||
| 62 | private static List<Extension> createEmptyExtensions() { | |
| 63 | return new ArrayList<>(); | |
| 64 | } | |
| 65 | ||
| 66 | /** | |
| 67 | * Instantiates a number of extensions to be applied when parsing. These | |
| 68 | * are typically typographic extensions that convert characters into | |
| 69 | * HTML entities. | |
| 70 | * | |
| 71 | * @return A {@link List} of {@link Extension} instances that | |
| 72 | * change the {@link Parser}'s behaviour. | |
| 73 | */ | |
| 74 | private static List<Extension> createDefaultExtensions() { | |
| 75 | final List<Extension> extensions = new ArrayList<>(); | |
| 76 | extensions.add( DefinitionExtension.create() ); | |
| 77 | extensions.add( StrikethroughSubscriptExtension.create() ); | |
| 78 | extensions.add( SuperscriptExtension.create() ); | |
| 79 | extensions.add( TablesExtension.create() ); | |
| 80 | extensions.add( TypographicExtension.create() ); | |
| 81 | return extensions; | |
| 37 | return new MarkdownProcessor( successor, context ); | |
| 82 | 38 | } |
| 83 | 39 | |
| ... | ||
| 91 | 47 | * formats can be edited. |
| 92 | 48 | * |
| 93 | * @param context Contains necessary information needed to create extensions | |
| 94 | * used by the Markdown parser. | |
| 95 | * @return {@link List} of extensions invoked when parsing Markdown. | |
| 49 | * @param extensions {@link List} of extensions invoked when parsing Markdown. | |
| 50 | * @param context Contains necessary information needed to create | |
| 51 | * extensions used by the Markdown parser. | |
| 96 | 52 | */ |
| 97 | private static List<Extension> createExtensions( | |
| 98 | final ProcessorContext context ) { | |
| 99 | final var extensions = createEmptyExtensions(); | |
| 100 | final var editorFile = context.getPath(); | |
| 101 | ||
| 53 | void init( | |
| 54 | final List<Extension> extensions, final ProcessorContext context ) { | |
| 55 | final var editorFile = context.getDocumentPath(); | |
| 102 | 56 | final var mediaType = MediaType.valueFrom( editorFile ); |
| 57 | final Processor<String> processor; | |
| 58 | ||
| 103 | 59 | if( mediaType == TEXT_R_MARKDOWN || mediaType == TEXT_R_XML ) { |
| 104 | extensions.add( RExtension.create( context ) ); | |
| 60 | final var rProcessor = new RProcessor( context ); | |
| 61 | extensions.add( RExtension.create( rProcessor ) ); | |
| 62 | processor = rProcessor; | |
| 63 | } | |
| 64 | else { | |
| 65 | processor = IDENTITY; | |
| 105 | 66 | } |
| 106 | 67 | |
| 107 | extensions.addAll( DEFAULT_EXTENSIONS ); | |
| 68 | // Add typographic, table, strikethrough, and similar extensions. | |
| 69 | super.init( extensions, context ); | |
| 70 | ||
| 108 | 71 | extensions.add( ImageLinkExtension.create( context ) ); |
| 109 | extensions.add( TeXExtension.create( context ) ); | |
| 72 | extensions.add( TeXExtension.create( context, processor ) ); | |
| 110 | 73 | extensions.add( FencedBlockExtension.create( context ) ); |
| 111 | 74 | extensions.add( CaretExtension.create( context ) ); |
| 112 | ||
| 113 | return extensions; | |
| 114 | } | |
| 115 | ||
| 116 | /** | |
| 117 | * Converts the given Markdown string into HTML, without the doctype, html, | |
| 118 | * head, and body tags. | |
| 119 | * | |
| 120 | * @param markdown The string to convert from Markdown to HTML. | |
| 121 | * @return The HTML representation of the Markdown document. | |
| 122 | */ | |
| 123 | @Override | |
| 124 | public String apply( final String markdown ) { | |
| 125 | return toHtml( parse( markdown ) ); | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * Returns the AST in the form of a node for the given Markdown document. This | |
| 130 | * can be used, for example, to determine if a hyperlink exists inside of a | |
| 131 | * paragraph. | |
| 132 | * | |
| 133 | * @param markdown The Markdown to convert into an AST. | |
| 134 | * @return The Markdown AST for the given text (usually a paragraph). | |
| 135 | */ | |
| 136 | public Node toNode( final String markdown ) { | |
| 137 | return parse( markdown ); | |
| 138 | } | |
| 139 | ||
| 140 | /** | |
| 141 | * Returns the result of converting the given AST into an HTML string. | |
| 142 | * | |
| 143 | * @param node The AST {@link Node} to convert to an HTML string. | |
| 144 | * @return The given {@link Node} as an HTML string. | |
| 145 | */ | |
| 146 | public String toHtml( final Node node ) { | |
| 147 | return getRenderer().render( node ); | |
| 148 | } | |
| 149 | ||
| 150 | /** | |
| 151 | * Helper method to create an AST given some Markdown. | |
| 152 | * | |
| 153 | * @param markdown The Markdown to parse. | |
| 154 | * @return The root node of the Markdown tree. | |
| 155 | */ | |
| 156 | private Node parse( final String markdown ) { | |
| 157 | return getParser().parse( markdown ); | |
| 158 | } | |
| 159 | ||
| 160 | /** | |
| 161 | * Creates the Markdown document processor. | |
| 162 | * | |
| 163 | * @return An instance of {@link IParse} for building abstract syntax trees. | |
| 164 | */ | |
| 165 | private IParse getParser() { | |
| 166 | return mParser; | |
| 167 | } | |
| 168 | ||
| 169 | private IRender getRenderer() { | |
| 170 | return mRenderer; | |
| 171 | 75 | } |
| 172 | 76 | } |
| 2 | 2 | package com.keenwrite.processors.markdown.extensions; |
| 3 | 3 | |
| 4 | import com.keenwrite.ExportFormat; | |
| 4 | 5 | import com.keenwrite.exceptions.MissingFileException; |
| 5 | 6 | import com.keenwrite.preferences.Workspace; |
| ... | ||
| 12 | 13 | import com.vladsch.flexmark.util.ast.Node; |
| 13 | 14 | import org.jetbrains.annotations.NotNull; |
| 14 | import org.renjin.repackaged.guava.base.Splitter; | |
| 15 | 15 | |
| 16 | 16 | import java.io.File; |
| 17 | 17 | import java.nio.file.Path; |
| 18 | import java.nio.file.Paths; | |
| 19 | 18 | |
| 19 | import static com.keenwrite.ExportFormat.NONE; | |
| 20 | 20 | import static com.keenwrite.StatusNotifier.clue; |
| 21 | 21 | import static com.keenwrite.preferences.Workspace.KEY_IMAGES_DIR; |
| 22 | 22 | import static com.keenwrite.preferences.Workspace.KEY_IMAGES_ORDER; |
| 23 | 23 | import static com.keenwrite.util.ProtocolScheme.getProtocol; |
| 24 | 24 | import static com.vladsch.flexmark.html.HtmlRenderer.Builder; |
| 25 | 25 | import static com.vladsch.flexmark.html.renderer.LinkStatus.VALID; |
| 26 | import static java.lang.String.format; | |
| 26 | import static org.renjin.repackaged.guava.base.Splitter.on; | |
| 27 | 27 | |
| 28 | 28 | /** |
| 29 | 29 | * Responsible for ensuring that images can be rendered relative to a path. |
| 30 | 30 | * This allows images to be located virtually anywhere. |
| 31 | 31 | */ |
| 32 | 32 | public class ImageLinkExtension extends HtmlRendererAdapter { |
| 33 | 33 | |
| 34 | 34 | private final Path mBaseDir; |
| 35 | 35 | private final Workspace mWorkspace; |
| 36 | private final ExportFormat mExportFormat; | |
| 36 | 37 | |
| 37 | 38 | private ImageLinkExtension( @NotNull final ProcessorContext context ) { |
| 38 | 39 | mBaseDir = context.getBaseDir(); |
| 39 | 40 | mWorkspace = context.getWorkspace(); |
| 41 | mExportFormat = context.getExportFormat(); | |
| 40 | 42 | } |
| 41 | 43 | |
| ... | ||
| 78 | 80 | } |
| 79 | 81 | |
| 82 | /** | |
| 83 | * Algorithm: | |
| 84 | * <ol> | |
| 85 | * <li>Accept remote URLs as valid links.</li> | |
| 86 | * <li>Accept existing readable files as valid links.</li> | |
| 87 | * <li>Accept non-{@link ExportFormat#NONE} exports as valid links.</li> | |
| 88 | * <li>Append the images dir to the edited file's dir (baseDir).</li> | |
| 89 | * <li>Search for images by extension.</li> | |
| 90 | * </ol> | |
| 91 | * | |
| 92 | * @param link The link URL to resolve. | |
| 93 | * @return The {@link ResolvedLink} instance used to render the link. | |
| 94 | */ | |
| 80 | 95 | private ResolvedLink resolve( final ResolvedLink link ) { |
| 81 | 96 | var uri = link.getUrl(); |
| 82 | 97 | final var protocol = getProtocol( uri ); |
| 83 | 98 | |
| 84 | if( protocol.isHttp() ) { | |
| 99 | if( protocol.isRemote() ) { | |
| 85 | 100 | return valid( link, uri ); |
| 86 | 101 | } |
| 102 | ||
| 103 | final var baseDir = getBaseDir(); | |
| 87 | 104 | |
| 88 | 105 | // Determine the fully-qualified file name (fqfn). |
| 89 | final var fqfn = Paths.get( getBaseDir().toString(), uri ).toFile(); | |
| 106 | final var fqfn = Path.of( baseDir.toString(), uri ).toFile(); | |
| 90 | 107 | |
| 91 | if( fqfn.isFile() ) { | |
| 108 | if( fqfn.isFile() && fqfn.canRead() ) { | |
| 92 | 109 | return valid( link, uri ); |
| 93 | 110 | } |
| 94 | 111 | |
| 95 | // At this point either the image directory is qualified or needs to be | |
| 96 | // qualified using the image prefix, as set in the user preferences. | |
| 97 | try { | |
| 98 | final var imagePrefix = getImagePrefix(); | |
| 99 | final var baseDir = getBaseDir().resolve( imagePrefix ); | |
| 112 | if( mExportFormat != NONE ) { | |
| 113 | return valid( link, uri ); | |
| 114 | } | |
| 100 | 115 | |
| 101 | final var imagePrefixDir = Path.of( baseDir.toString(), uri ); | |
| 102 | final var suffixes = getImageExtensions(); | |
| 103 | boolean missing = true; | |
| 116 | try { | |
| 117 | // Compute the path to the image file. The base directory should | |
| 118 | // be an absolute path to the file being edited, without an extension. | |
| 119 | final var imagesDir = getUserImagesDir(); | |
| 120 | final var relativeDir = imagesDir.toString().isEmpty() | |
| 121 | ? imagesDir : baseDir.relativize( imagesDir ); | |
| 122 | final var imageFile = Path.of( | |
| 123 | baseDir.toString(), relativeDir.toString(), uri ); | |
| 104 | 124 | |
| 105 | // Iterate over the user's preferred image file type extensions. | |
| 106 | for( final var ext : Splitter.on( ' ' ).split( suffixes ) ) { | |
| 107 | final var imagePath = format( "%s.%s", imagePrefixDir, ext ); | |
| 108 | final var file = new File( imagePath ); | |
| 125 | for( final var ext : getImageExtensions() ) { | |
| 126 | var file = new File( imageFile.toString() + '.' + ext ); | |
| 109 | 127 | |
| 110 | if( file.exists() ) { | |
| 111 | uri += '.' + ext; | |
| 112 | final var path = Path.of( imagePrefix.toString(), uri ); | |
| 113 | uri = path.normalize().toString(); | |
| 114 | missing = false; | |
| 115 | break; | |
| 128 | if( file.exists() && file.canRead() ) { | |
| 129 | uri = file.toURI().toString(); | |
| 130 | return valid( link, uri ); | |
| 116 | 131 | } |
| 117 | } | |
| 118 | ||
| 119 | if( missing ) { | |
| 120 | throw new MissingFileException( imagePrefixDir + ".*" ); | |
| 121 | 132 | } |
| 122 | 133 | |
| 123 | return valid( link, uri ); | |
| 134 | throw new MissingFileException( imageFile + ".*" ); | |
| 124 | 135 | } catch( final Exception ex ) { |
| 125 | 136 | clue( ex ); |
| ... | ||
| 133 | 144 | } |
| 134 | 145 | |
| 135 | private Path getImagePrefix() { | |
| 146 | private Path getUserImagesDir() { | |
| 136 | 147 | return mWorkspace.toFile( KEY_IMAGES_DIR ).toPath(); |
| 137 | 148 | } |
| 138 | 149 | |
| 139 | private String getImageExtensions() { | |
| 140 | return mWorkspace.toString( KEY_IMAGES_ORDER ); | |
| 150 | private Iterable<String> getImageExtensions() { | |
| 151 | return on( ' ' ).split( mWorkspace.toString( KEY_IMAGES_ORDER ) ); | |
| 141 | 152 | } |
| 142 | 153 | |
| 2 | 2 | package com.keenwrite.processors.markdown.extensions.r; |
| 3 | 3 | |
| 4 | import com.keenwrite.processors.*; | |
| 4 | import com.keenwrite.processors.Processor; | |
| 5 | import com.keenwrite.processors.r.InlineRProcessor; | |
| 6 | import com.keenwrite.processors.r.RProcessor; | |
| 5 | 7 | import com.keenwrite.sigils.RSigilOperator; |
| 6 | 8 | import com.vladsch.flexmark.ast.Text; |
| ... | ||
| 17 | 19 | import java.util.Map; |
| 18 | 20 | |
| 19 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | |
| 20 | 21 | import static com.vladsch.flexmark.parser.Parser.Builder; |
| 21 | 22 | import static com.vladsch.flexmark.parser.Parser.ParserExtension; |
| ... | ||
| 31 | 32 | public final class RExtension implements ParserExtension { |
| 32 | 33 | private final InlineParserFactory FACTORY = CustomParser::new; |
| 33 | ||
| 34 | private final Processor<String> mProcessor; | |
| 35 | private final InlineRProcessor mInlineRProcessor; | |
| 36 | private boolean mReady; | |
| 34 | private final RProcessor mProcessor; | |
| 37 | 35 | |
| 38 | private RExtension( final ProcessorContext context ) { | |
| 39 | final var irp = new InlineRProcessor( IDENTITY, context ); | |
| 40 | final var rvp = new RVariableProcessor( irp, context ); | |
| 41 | mProcessor = new ExecutorProcessor<>( rvp ); | |
| 42 | mInlineRProcessor = irp; | |
| 36 | private RExtension( final RProcessor processor ) { | |
| 37 | mProcessor = processor; | |
| 43 | 38 | } |
| 44 | 39 | |
| 45 | 40 | /** |
| 46 | 41 | * Creates an extension capable of intercepting R code blocks and preventing |
| 47 | 42 | * them from being converted into HTML {@code <code>} elements. |
| 48 | 43 | */ |
| 49 | public static RExtension create( final ProcessorContext context ) { | |
| 50 | return new RExtension( context ); | |
| 44 | public static RExtension create( final RProcessor processor ) { | |
| 45 | return new RExtension( processor ); | |
| 51 | 46 | } |
| 52 | 47 | |
| ... | ||
| 86 | 81 | referenceLinkProcessors, |
| 87 | 82 | inlineParserExtensions ); |
| 88 | mReady = mInlineRProcessor.init(); | |
| 83 | mProcessor.init(); | |
| 89 | 84 | } |
| 90 | 85 | |
| ... | ||
| 103 | 98 | final var foundTicks = super.parseBackticks(); |
| 104 | 99 | |
| 105 | if( foundTicks && mReady ) { | |
| 100 | if( foundTicks && mProcessor.isReady() ) { | |
| 106 | 101 | final var blockNode = getBlock(); |
| 107 | 102 | final var codeNode = blockNode.getLastChild(); |
| 3 | 3 | |
| 4 | 4 | import com.keenwrite.processors.ExecutorProcessor; |
| 5 | import com.keenwrite.processors.InlineRProcessor; | |
| 5 | import com.keenwrite.processors.r.InlineRProcessor; | |
| 6 | 6 | import com.keenwrite.processors.markdown.MarkdownProcessor; |
| 7 | 7 | import com.keenwrite.processors.markdown.extensions.tex.TeXExtension; |
| 3 | 3 | |
| 4 | 4 | import com.keenwrite.ExportFormat; |
| 5 | import com.keenwrite.processors.Processor; | |
| 5 | 6 | import com.keenwrite.processors.ProcessorContext; |
| 6 | 7 | import com.keenwrite.processors.markdown.extensions.HtmlRendererAdapter; |
| ... | ||
| 30 | 31 | private final ExportFormat mExportFormat; |
| 31 | 32 | |
| 32 | private TeXExtension( final ProcessorContext context ) { | |
| 33 | private final Processor<String> mProcessor; | |
| 34 | ||
| 35 | private TeXExtension( | |
| 36 | final ProcessorContext context, final Processor<String> processor ) { | |
| 33 | 37 | mExportFormat = context.getExportFormat(); |
| 38 | mProcessor = processor; | |
| 34 | 39 | } |
| 35 | 40 | |
| 36 | 41 | /** |
| 37 | 42 | * Creates an extension capable of handling delimited TeX code in Markdown. |
| 38 | 43 | * |
| 39 | 44 | * @return The new {@link TeXExtension}, never {@code null}. |
| 40 | 45 | */ |
| 41 | public static TeXExtension create( final ProcessorContext context ) { | |
| 42 | return new TeXExtension( context ); | |
| 46 | public static TeXExtension create( | |
| 47 | final ProcessorContext context, final Processor<String> processor ) { | |
| 48 | return new TeXExtension( context, processor ); | |
| 43 | 49 | } |
| 44 | 50 | |
| ... | ||
| 53 | 59 | @NotNull final String rendererType ) { |
| 54 | 60 | if( "HTML".equalsIgnoreCase( rendererType ) ) { |
| 55 | builder.nodeRendererFactory( new Factory( mExportFormat ) ); | |
| 61 | builder.nodeRendererFactory( new Factory( mExportFormat, mProcessor ) ); | |
| 56 | 62 | } |
| 57 | 63 | } |
| 4 | 4 | import com.keenwrite.ExportFormat; |
| 5 | 5 | import com.keenwrite.preview.SvgRasterizer; |
| 6 | import com.keenwrite.processors.Processor; | |
| 6 | 7 | import com.vladsch.flexmark.html.HtmlWriter; |
| 7 | 8 | import com.vladsch.flexmark.html.renderer.NodeRenderer; |
| ... | ||
| 14 | 15 | import org.jetbrains.annotations.Nullable; |
| 15 | 16 | |
| 17 | import java.util.Map; | |
| 16 | 18 | import java.util.Set; |
| 17 | 19 | |
| 20 | import static com.keenwrite.ExportFormat.*; | |
| 18 | 21 | import static com.keenwrite.preview.MathRenderer.MATH_RENDERER; |
| 19 | 22 | import static com.keenwrite.processors.markdown.extensions.tex.TexNode.*; |
| 20 | 23 | |
| 21 | 24 | public class TexNodeRenderer { |
| 25 | private static final Map<ExportFormat, RendererFacade> EXPORT_RENDERERS = | |
| 26 | Map.of( | |
| 27 | HTML_TEX_SVG, new TexSvgNodeRenderer(), | |
| 28 | HTML_TEX_DELIMITED, new TexDelimNodeRenderer(), | |
| 29 | MARKDOWN_PLAIN, new TexDelimNodeRenderer(), | |
| 30 | NONE, new TexElementNodeRenderer() | |
| 31 | ); | |
| 22 | 32 | |
| 23 | 33 | public static class Factory implements NodeRendererFactory { |
| 24 | private final ExportFormat mExportFormat; | |
| 34 | private final RendererFacade mNodeRenderer; | |
| 25 | 35 | |
| 26 | public Factory( final ExportFormat exportFormat ) { | |
| 27 | mExportFormat = exportFormat; | |
| 36 | public Factory( | |
| 37 | final ExportFormat exportFormat, final Processor<String> processor ) { | |
| 38 | mNodeRenderer = EXPORT_RENDERERS.get( exportFormat ); | |
| 39 | mNodeRenderer.setProcessor( processor ); | |
| 28 | 40 | } |
| 29 | 41 | |
| 30 | 42 | @NotNull |
| 31 | 43 | @Override |
| 32 | 44 | public NodeRenderer apply( @NotNull DataHolder options ) { |
| 33 | return switch( mExportFormat ) { | |
| 34 | case HTML_TEX_SVG -> new TexSvgNodeRenderer(); | |
| 35 | case HTML_TEX_DELIMITED, MARKDOWN_PLAIN -> new TexDelimNodeRenderer(); | |
| 36 | case NONE -> new TexElementNodeRenderer(); | |
| 37 | }; | |
| 45 | return mNodeRenderer; | |
| 38 | 46 | } |
| 39 | 47 | } |
| 40 | 48 | |
| 41 | private static abstract class AbstractTexNodeRenderer | |
| 42 | implements NodeRenderer { | |
| 49 | private static abstract class RendererFacade | |
| 50 | implements NodeRenderer { | |
| 51 | private Processor<String> mProcessor; | |
| 43 | 52 | |
| 44 | 53 | @Override |
| ... | ||
| 59 | 68 | final NodeRendererContext context, |
| 60 | 69 | final HtmlWriter html ); |
| 70 | ||
| 71 | private void setProcessor( final Processor<String> processor ) { | |
| 72 | mProcessor = processor; | |
| 73 | } | |
| 74 | ||
| 75 | Processor<String> getProcessor() { | |
| 76 | return mProcessor; | |
| 77 | } | |
| 61 | 78 | } |
| 62 | 79 | |
| 63 | 80 | /** |
| 64 | 81 | * Responsible for rendering a TeX node as an HTML {@code <tex>} |
| 65 | 82 | * element. This is the default behaviour. |
| 66 | 83 | */ |
| 67 | private static class TexElementNodeRenderer extends AbstractTexNodeRenderer { | |
| 84 | private static class TexElementNodeRenderer extends RendererFacade { | |
| 68 | 85 | void render( final TexNode node, |
| 69 | 86 | final NodeRendererContext context, |
| 70 | 87 | final HtmlWriter html ) { |
| 71 | 88 | html.tag( HTML_TEX ); |
| 72 | html.raw( node.getText() ); | |
| 89 | html.raw( getProcessor().apply( node.getText().toString() ) ); | |
| 73 | 90 | html.closeTag( HTML_TEX ); |
| 74 | 91 | } |
| 75 | 92 | } |
| 76 | 93 | |
| 77 | 94 | /** |
| 78 | 95 | * Responsible for rendering a TeX node as an HTML {@code <svg>} |
| 79 | 96 | * element. |
| 80 | 97 | */ |
| 81 | private static class TexSvgNodeRenderer extends AbstractTexNodeRenderer { | |
| 98 | private static class TexSvgNodeRenderer extends RendererFacade { | |
| 82 | 99 | void render( final TexNode node, |
| 83 | 100 | final NodeRendererContext context, |
| 84 | 101 | final HtmlWriter html ) { |
| 85 | 102 | final var tex = node.getText().toStringOrNull(); |
| 86 | final var doc = MATH_RENDERER.render( tex == null ? "" : tex ); | |
| 103 | final var doc = MATH_RENDERER.render( | |
| 104 | tex == null ? "" : getProcessor().apply( tex ) ); | |
| 87 | 105 | final var svg = SvgRasterizer.toSvg( doc.getDocumentElement() ); |
| 88 | 106 | html.raw( svg ); |
| 89 | 107 | } |
| 90 | 108 | } |
| 91 | 109 | |
| 92 | 110 | /** |
| 93 | 111 | * Responsible for rendering a TeX node as text bracketed by $ tokens. |
| 94 | 112 | */ |
| 95 | private static class TexDelimNodeRenderer extends AbstractTexNodeRenderer { | |
| 113 | private static class TexDelimNodeRenderer extends RendererFacade { | |
| 96 | 114 | void render( final TexNode node, |
| 97 | 115 | final NodeRendererContext context, |
| 98 | 116 | final HtmlWriter html ) { |
| 99 | 117 | html.raw( TOKEN_OPEN ); |
| 100 | html.raw( node.getText() ); | |
| 118 | html.raw( getProcessor().apply( node.getText().toString() ) ); | |
| 101 | 119 | html.raw( TOKEN_CLOSE ); |
| 102 | 120 | } |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.r; | |
| 3 | ||
| 4 | import com.keenwrite.preferences.Workspace; | |
| 5 | import com.keenwrite.processors.DefinitionProcessor; | |
| 6 | import com.keenwrite.processors.Processor; | |
| 7 | import com.keenwrite.processors.ProcessorContext; | |
| 8 | import com.keenwrite.processors.markdown.extensions.r.ROutputProcessor; | |
| 9 | import com.keenwrite.util.BoundedCache; | |
| 10 | import javafx.beans.property.Property; | |
| 11 | ||
| 12 | import javax.script.ScriptEngine; | |
| 13 | import javax.script.ScriptEngineManager; | |
| 14 | import java.io.File; | |
| 15 | import java.nio.file.Path; | |
| 16 | import java.util.Map; | |
| 17 | import java.util.concurrent.atomic.AtomicBoolean; | |
| 18 | ||
| 19 | import static com.keenwrite.Constants.STATUS_PARSE_ERROR; | |
| 20 | import static com.keenwrite.Messages.get; | |
| 21 | import static com.keenwrite.StatusNotifier.clue; | |
| 22 | import static com.keenwrite.preferences.Workspace.*; | |
| 23 | import static com.keenwrite.processors.text.TextReplacementFactory.replace; | |
| 24 | import static com.keenwrite.sigils.RSigilOperator.PREFIX; | |
| 25 | import static com.keenwrite.sigils.RSigilOperator.SUFFIX; | |
| 26 | import static java.lang.Math.max; | |
| 27 | import static java.lang.Math.min; | |
| 28 | import static java.lang.String.format; | |
| 29 | ||
| 30 | /** | |
| 31 | * Transforms a document containing R statements into Markdown. | |
| 32 | */ | |
| 33 | public final class InlineRProcessor extends DefinitionProcessor { | |
| 34 | private final Processor<String> mPostProcessor = new ROutputProcessor(); | |
| 35 | ||
| 36 | /** | |
| 37 | * Where to put document inline evaluated R expressions, constrained to | |
| 38 | * avoid running out of memory. | |
| 39 | */ | |
| 40 | private final Map<String, String> mEvalCache = | |
| 41 | new BoundedCache<>( 512 ); | |
| 42 | ||
| 43 | private static final ScriptEngine ENGINE = | |
| 44 | (new ScriptEngineManager()).getEngineByName( "Renjin" ); | |
| 45 | ||
| 46 | private static final int PREFIX_LENGTH = PREFIX.length(); | |
| 47 | ||
| 48 | private final AtomicBoolean mDirty = new AtomicBoolean( false ); | |
| 49 | ||
| 50 | private final Workspace mWorkspace; | |
| 51 | ||
| 52 | /** | |
| 53 | * Constructs a processor capable of evaluating R statements. | |
| 54 | * | |
| 55 | * @param successor Subsequent link in the processing chain. | |
| 56 | * @param context Contains resolved definitions map. | |
| 57 | */ | |
| 58 | public InlineRProcessor( | |
| 59 | final Processor<String> successor, | |
| 60 | final ProcessorContext context ) { | |
| 61 | super( successor, context ); | |
| 62 | ||
| 63 | mWorkspace = context.getWorkspace(); | |
| 64 | ||
| 65 | bootstrapScriptProperty().addListener( | |
| 66 | ( __, oldScript, newScript ) -> setDirty( true ) ); | |
| 67 | workingDirectoryProperty().addListener( | |
| 68 | ( __, oldScript, newScript ) -> setDirty( true ) ); | |
| 69 | ||
| 70 | // TODO: Watch the "R" property keys in the workspace, directly. | |
| 71 | ||
| 72 | // If the user saves the preferences, make sure that any R-related settings | |
| 73 | // changes are applied. | |
| 74 | // getWorkspace().addSaveEventHandler( ( handler ) -> { | |
| 75 | // if( isDirty() ) { | |
| 76 | // init(); | |
| 77 | // setDirty( false ); | |
| 78 | // } | |
| 79 | // } ); | |
| 80 | ||
| 81 | init(); | |
| 82 | } | |
| 83 | ||
| 84 | /** | |
| 85 | * Initialises the R code so that R can find imported libraries. Note that | |
| 86 | * any existing R functionality will not be overwritten if this method is | |
| 87 | * called multiple times. | |
| 88 | * | |
| 89 | * @return {@code true} if initialization completed and all variables were | |
| 90 | * replaced; {@code false} if any variables remain. | |
| 91 | */ | |
| 92 | public boolean init() { | |
| 93 | final var bootstrap = getBootstrapScript(); | |
| 94 | ||
| 95 | if( !bootstrap.isBlank() ) { | |
| 96 | final var wd = getWorkingDirectory(); | |
| 97 | final var dir = wd.toString().replace( '\\', '/' ); | |
| 98 | final var map = getDefinitions(); | |
| 99 | final var defBegan = mWorkspace.toString( KEY_DEF_DELIM_BEGAN ); | |
| 100 | final var defEnded = mWorkspace.toString( KEY_DEF_DELIM_ENDED ); | |
| 101 | ||
| 102 | map.put( defBegan + "application.r.working.directory" + defEnded, dir ); | |
| 103 | ||
| 104 | final var replaced = replace( bootstrap, map ); | |
| 105 | final var bIndex = replaced.indexOf( defBegan ); | |
| 106 | ||
| 107 | // If there's a delimiter in the replaced text it means not all variables | |
| 108 | // are bound, which is an error. | |
| 109 | if( bIndex >= 0 ) { | |
| 110 | var eIndex = replaced.indexOf( defEnded ); | |
| 111 | eIndex = (eIndex == -1) ? replaced.length() - 1 : max( bIndex, eIndex ); | |
| 112 | ||
| 113 | final var def = replaced.substring( | |
| 114 | bIndex + defBegan.length(), eIndex ); | |
| 115 | clue( "Main.status.error.bootstrap.eval", | |
| 116 | format( "%s%s%s", defBegan, def, defEnded ) ); | |
| 117 | ||
| 118 | return false; | |
| 119 | } | |
| 120 | else { | |
| 121 | eval( replaced ); | |
| 122 | } | |
| 123 | } | |
| 124 | ||
| 125 | return true; | |
| 126 | } | |
| 127 | ||
| 128 | /** | |
| 129 | * Empties the cache. | |
| 130 | */ | |
| 131 | public void clear() { | |
| 132 | mEvalCache.clear(); | |
| 133 | } | |
| 134 | ||
| 135 | /** | |
| 136 | * Sets the dirty flag to indicate that the bootstrap script or working | |
| 137 | * directory has been modified. Upon saving the preferences, if this flag | |
| 138 | * is true, then {@link #init()} will be called to reload the R environment. | |
| 139 | * | |
| 140 | * @param dirty Set to true to reload changes upon closing preferences. | |
| 141 | */ | |
| 142 | private void setDirty( final boolean dirty ) { | |
| 143 | mDirty.set( dirty ); | |
| 144 | } | |
| 145 | ||
| 146 | /** | |
| 147 | * Answers whether R-related settings have been modified. | |
| 148 | * | |
| 149 | * @return {@code true} when the settings have changed. | |
| 150 | */ | |
| 151 | private boolean isDirty() { | |
| 152 | return mDirty.get(); | |
| 153 | } | |
| 154 | ||
| 155 | /** | |
| 156 | * Evaluates all R statements in the source document and inserts the | |
| 157 | * calculated value into the generated document. | |
| 158 | * | |
| 159 | * @param text The document text that includes variables that should be | |
| 160 | * replaced with values when rendered as HTML. | |
| 161 | * @return The generated document with output from all R statements | |
| 162 | * substituted with value returned from their execution. | |
| 163 | */ | |
| 164 | @Override | |
| 165 | public String apply( final String text ) { | |
| 166 | final int length = text.length(); | |
| 167 | ||
| 168 | // The * 2 is a wild guess at the ratio of R statements to the length | |
| 169 | // of text produced by those statements. | |
| 170 | final StringBuilder sb = new StringBuilder( length * 2 ); | |
| 171 | ||
| 172 | int prevIndex = 0; | |
| 173 | int currIndex = text.indexOf( PREFIX ); | |
| 174 | ||
| 175 | while( currIndex >= 0 ) { | |
| 176 | // Copy everything up to, but not including, the opening token. | |
| 177 | sb.append( text, prevIndex, currIndex ); | |
| 178 | ||
| 179 | // Jump to the start of the R statement. | |
| 180 | prevIndex = currIndex + PREFIX_LENGTH; | |
| 181 | ||
| 182 | // Find the closing token, without indexing past the text boundary. | |
| 183 | currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) ); | |
| 184 | ||
| 185 | // Only evaluate inline R statements that have end delimiters. | |
| 186 | if( currIndex > 1 ) { | |
| 187 | // Extract the inline R statement to be evaluated. | |
| 188 | final var r = text.substring( prevIndex, currIndex ); | |
| 189 | ||
| 190 | // Pass the R statement into the R engine for evaluation. | |
| 191 | try { | |
| 192 | // Append the string representation of the result into the text. | |
| 193 | sb.append( evalCached( r ) ); | |
| 194 | } catch( final Exception ex ) { | |
| 195 | // Inform the user that there was a problem. | |
| 196 | clue( STATUS_PARSE_ERROR, ex.getMessage(), currIndex ); | |
| 197 | ||
| 198 | // If the string couldn't be parsed using R, append the statement | |
| 199 | // that failed to parse, instead of its evaluated value. | |
| 200 | sb.append( PREFIX ).append( r ).append( SUFFIX ); | |
| 201 | } | |
| 202 | ||
| 203 | // Retain the R statement's ending position in the text. | |
| 204 | prevIndex = currIndex + 1; | |
| 205 | } | |
| 206 | ||
| 207 | // Find the start of the next inline R statement. | |
| 208 | currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) ); | |
| 209 | } | |
| 210 | ||
| 211 | // Copy from the previous index to the end of the string. | |
| 212 | return sb.append( text.substring( min( prevIndex, length ) ) ).toString(); | |
| 213 | } | |
| 214 | ||
| 215 | /** | |
| 216 | * Look up an R expression from the cache then return the resulting object. | |
| 217 | * If the R expression hasn't been cached, it'll first be evaluated. | |
| 218 | * | |
| 219 | * @param r The expression to evaluate. | |
| 220 | * @return The object resulting from the evaluation. | |
| 221 | */ | |
| 222 | private String evalCached( final String r ) { | |
| 223 | return mEvalCache.computeIfAbsent( r, __ -> evalHtml( r ) ); | |
| 224 | } | |
| 225 | ||
| 226 | /** | |
| 227 | * Converts the given string to HTML, trimming new lines, and inlining | |
| 228 | * the text if it is a paragraph. Otherwise, the resulting HTML is most likely | |
| 229 | * complex (e.g., a Markdown table) and should be rendered as its HTML | |
| 230 | * equivalent. | |
| 231 | * | |
| 232 | * @param r The R expression to evaluate then convert to HTML. | |
| 233 | * @return The result from the R expression as an HTML element. | |
| 234 | */ | |
| 235 | private String evalHtml( final String r ) { | |
| 236 | return mPostProcessor.apply( eval( r ) ); | |
| 237 | } | |
| 238 | ||
| 239 | /** | |
| 240 | * Evaluate an R expression and return the resulting object. | |
| 241 | * | |
| 242 | * @param r The expression to evaluate. | |
| 243 | * @return The object resulting from the evaluation. | |
| 244 | */ | |
| 245 | private String eval( final String r ) { | |
| 246 | try { | |
| 247 | return ENGINE.eval( r ).toString(); | |
| 248 | } catch( final Exception ex ) { | |
| 249 | final var expr = r.substring( 0, min( r.length(), 50 ) ); | |
| 250 | clue( get( "Main.status.error.r", expr, ex.getMessage() ), ex ); | |
| 251 | return ""; | |
| 252 | } | |
| 253 | } | |
| 254 | ||
| 255 | /** | |
| 256 | * Return the given path if not {@code null}, otherwise return the path to | |
| 257 | * the user's directory. | |
| 258 | * | |
| 259 | * @return A non-null path. | |
| 260 | */ | |
| 261 | private Path getWorkingDirectory() { | |
| 262 | return workingDirectoryProperty().getValue().toPath(); | |
| 263 | } | |
| 264 | ||
| 265 | private Property<File> workingDirectoryProperty() { | |
| 266 | return getWorkspace().fileProperty( KEY_R_DIR ); | |
| 267 | } | |
| 268 | ||
| 269 | /** | |
| 270 | * Loads the R init script from the application's persisted preferences. | |
| 271 | * | |
| 272 | * @return A non-null string, possibly empty. | |
| 273 | */ | |
| 274 | private String getBootstrapScript() { | |
| 275 | return bootstrapScriptProperty().getValue(); | |
| 276 | } | |
| 277 | ||
| 278 | private Property<String> bootstrapScriptProperty() { | |
| 279 | return getWorkspace().valuesProperty( KEY_R_SCRIPT ); | |
| 280 | } | |
| 281 | ||
| 282 | private Workspace getWorkspace() { | |
| 283 | return mWorkspace; | |
| 284 | } | |
| 285 | } | |
| 1 | 286 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.r; | |
| 3 | ||
| 4 | import com.keenwrite.processors.ExecutorProcessor; | |
| 5 | import com.keenwrite.processors.Processor; | |
| 6 | import com.keenwrite.processors.ProcessorContext; | |
| 7 | ||
| 8 | import static com.keenwrite.processors.IdentityProcessor.IDENTITY; | |
| 9 | ||
| 10 | /** | |
| 11 | * Responsible for processing R statements within a text block. | |
| 12 | */ | |
| 13 | public final class RProcessor extends ExecutorProcessor<String> { | |
| 14 | private final Processor<String> mProcessor; | |
| 15 | private final InlineRProcessor mInlineRProcessor; | |
| 16 | private volatile boolean mReady; | |
| 17 | ||
| 18 | public RProcessor( final ProcessorContext context ) { | |
| 19 | final var irp = new InlineRProcessor( IDENTITY, context ); | |
| 20 | final var rvp = new RVariableProcessor( irp, context ); | |
| 21 | mProcessor = new ExecutorProcessor<>( rvp ); | |
| 22 | mInlineRProcessor = irp; | |
| 23 | } | |
| 24 | ||
| 25 | public void init() { | |
| 26 | mReady = mInlineRProcessor.init(); | |
| 27 | } | |
| 28 | ||
| 29 | public boolean isReady() { | |
| 30 | return mReady; | |
| 31 | } | |
| 32 | ||
| 33 | public String apply( final String text ) { | |
| 34 | return mProcessor.apply( text ); | |
| 35 | } | |
| 36 | } | |
| 1 | 37 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite.processors.r; | |
| 3 | ||
| 4 | import com.keenwrite.preferences.Workspace; | |
| 5 | import com.keenwrite.processors.DefinitionProcessor; | |
| 6 | import com.keenwrite.processors.ProcessorContext; | |
| 7 | import com.keenwrite.sigils.RSigilOperator; | |
| 8 | import com.keenwrite.sigils.SigilOperator; | |
| 9 | import com.keenwrite.sigils.YamlSigilOperator; | |
| 10 | ||
| 11 | import java.util.HashMap; | |
| 12 | import java.util.Map; | |
| 13 | ||
| 14 | import static com.keenwrite.preferences.Workspace.*; | |
| 15 | ||
| 16 | /** | |
| 17 | * Converts the keys of the resolved map from default form to R form, then | |
| 18 | * performs a substitution on the text. The default R variable syntax is | |
| 19 | * {@code v$tree$leaf}. | |
| 20 | */ | |
| 21 | public final class RVariableProcessor extends DefinitionProcessor { | |
| 22 | ||
| 23 | private final SigilOperator mSigilOperator; | |
| 24 | ||
| 25 | public RVariableProcessor( | |
| 26 | final InlineRProcessor irp, final ProcessorContext context ) { | |
| 27 | super( irp, context ); | |
| 28 | mSigilOperator = createSigilOperator( context.getWorkspace() ); | |
| 29 | } | |
| 30 | ||
| 31 | /** | |
| 32 | * Returns the R-based version of the interpolated variable definitions. | |
| 33 | * | |
| 34 | * @return Variable names transmogrified from the default syntax to R syntax. | |
| 35 | */ | |
| 36 | @Override | |
| 37 | protected Map<String, String> getDefinitions() { | |
| 38 | return entoken( super.getDefinitions() ); | |
| 39 | } | |
| 40 | ||
| 41 | /** | |
| 42 | * Converts the given map from regular variables to R variables. | |
| 43 | * | |
| 44 | * @param map Map of variable names to values. | |
| 45 | * @return Map of R variables. | |
| 46 | */ | |
| 47 | private Map<String, String> entoken( final Map<String, String> map ) { | |
| 48 | final var rMap = new HashMap<String, String>( map.size() ); | |
| 49 | ||
| 50 | for( final var entry : map.entrySet() ) { | |
| 51 | final var key = entry.getKey(); | |
| 52 | rMap.put( mSigilOperator.entoken( key ), escape( map.get( key ) ) ); | |
| 53 | } | |
| 54 | ||
| 55 | return rMap; | |
| 56 | } | |
| 57 | ||
| 58 | private String escape( final String value ) { | |
| 59 | return '\'' + escape( value, '\'', "\\'" ) + '\''; | |
| 60 | } | |
| 61 | ||
| 62 | /** | |
| 63 | * TODO: Make generic method for replacing text. | |
| 64 | * | |
| 65 | * @param haystack Search this string for the needle, must not be null. | |
| 66 | * @param needle The character to find in the haystack. | |
| 67 | * @param thread Replace the needle with this text, if the needle is found. | |
| 68 | * @return The haystack with the all instances of needle replaced with thread. | |
| 69 | */ | |
| 70 | @SuppressWarnings("SameParameterValue") | |
| 71 | private String escape( | |
| 72 | final String haystack, final char needle, final String thread ) { | |
| 73 | int end = haystack.indexOf( needle ); | |
| 74 | ||
| 75 | if( end < 0 ) { | |
| 76 | return haystack; | |
| 77 | } | |
| 78 | ||
| 79 | final int length = haystack.length(); | |
| 80 | int start = 0; | |
| 81 | ||
| 82 | // Replace up to 32 occurrences before the string reallocates its buffer. | |
| 83 | final var sb = new StringBuilder( length + 32 ); | |
| 84 | ||
| 85 | while( end >= 0 ) { | |
| 86 | sb.append( haystack, start, end ).append( thread ); | |
| 87 | start = end + 1; | |
| 88 | end = haystack.indexOf( needle, start ); | |
| 89 | } | |
| 90 | ||
| 91 | return sb.append( haystack.substring( start ) ).toString(); | |
| 92 | } | |
| 93 | ||
| 94 | private SigilOperator createSigilOperator( final Workspace workspace ) { | |
| 95 | final var tokens = workspace.toTokens( | |
| 96 | KEY_R_DELIM_BEGAN, KEY_R_DELIM_ENDED ); | |
| 97 | final var antecedent = createDefinitionOperator( workspace ); | |
| 98 | return new RSigilOperator( tokens, antecedent ); | |
| 99 | } | |
| 100 | ||
| 101 | private SigilOperator createDefinitionOperator( | |
| 102 | final Workspace workspace ) { | |
| 103 | final var tokens = workspace.toTokens( | |
| 104 | KEY_DEF_DELIM_BEGAN, KEY_DEF_DELIM_ENDED ); | |
| 105 | return new YamlSigilOperator( tokens ); | |
| 106 | } | |
| 107 | } | |
| 1 | 108 |
| 19 | 19 | * a {@link Trie} for efficiency. |
| 20 | 20 | */ |
| 21 | public class SearchModel { | |
| 21 | public final class SearchModel { | |
| 22 | 22 | private final ObjectProperty<IndexRange> mMatchOffset = |
| 23 | 23 | new SimpleObjectProperty<>(); |
| 7 | 7 | * Brackets variable names between {@link #PREFIX} and {@link #SUFFIX} sigils. |
| 8 | 8 | */ |
| 9 | public class RSigilOperator extends SigilOperator { | |
| 9 | public final class RSigilOperator extends SigilOperator { | |
| 10 | 10 | public static final char KEY_SEPARATOR_R = '$'; |
| 11 | 11 |
| 5 | 5 | * Brackets definition keys with token delimiters. |
| 6 | 6 | */ |
| 7 | public class YamlSigilOperator extends SigilOperator { | |
| 7 | public final class YamlSigilOperator extends SigilOperator { | |
| 8 | 8 | public static final char KEY_SEPARATOR_DEF = '.'; |
| 9 | 9 | |
| ... | ||
| 36 | 36 | |
| 37 | 37 | /** |
| 38 | * Removes start and stop definition key delimiters from the given key. This | |
| 39 | * method does not check for delimiters, only that there are sufficient | |
| 40 | * characters to remove from either end of the given key. | |
| 38 | * Removes start and stop definition key delimiters from the given key. | |
| 41 | 39 | * |
| 42 | * @param key The key adorned with start and stop definition tokens. | |
| 40 | * @param key The key that may have start and stop tokens. | |
| 43 | 41 | * @return The given key with the delimiters removed. |
| 44 | 42 | */ |
| 45 | 43 | public String detoken( final String key ) { |
| 46 | final int beganLen = getBegan().length(); | |
| 47 | final int endedLen = getEnded().length(); | |
| 44 | final var began = getBegan(); | |
| 45 | final var ended = getEnded(); | |
| 46 | final int bLength = began.length(); | |
| 47 | final int eLength = ended.length(); | |
| 48 | final var bIndex = key.indexOf( began ); | |
| 49 | final var eIndex = key.indexOf( ended, bIndex ); | |
| 50 | final var kLength = key.length(); | |
| 48 | 51 | |
| 49 | return key.length() > beganLen + endedLen | |
| 50 | ? key.substring( beganLen, key.length() - endedLen ) | |
| 51 | : key; | |
| 52 | return key.substring( | |
| 53 | bIndex == -1 ? 0 : bLength, eIndex == -1 ? kLength : kLength - eLength ); | |
| 52 | 54 | } |
| 53 | 55 | } |
| 12 | 12 | * spell checking and indicates that any given lexeme is in the lexicon. |
| 13 | 13 | */ |
| 14 | public class PermissiveSpeller implements SpellChecker { | |
| 14 | public final class PermissiveSpeller implements SpellChecker { | |
| 15 | 15 | /** |
| 16 | 16 | * Returns {@code true}, ignoring the given word. |
| 21 | 21 | * Responsible for checking the spelling of a document being edited. |
| 22 | 22 | */ |
| 23 | public class TextEditorSpeller { | |
| 23 | public final class TextEditorSpeller { | |
| 24 | 24 | /** |
| 25 | 25 | * Only load the dictionary into memory once, because it's huge. |
| 23 | 23 | * Defines actions the user can take through GUI interactions |
| 24 | 24 | */ |
| 25 | public class Action implements MenuAction { | |
| 25 | public final class Action implements MenuAction { | |
| 26 | 26 | private final String mText; |
| 27 | 27 | private final KeyCombination mAccelerator; |
| 43 | 43 | */ |
| 44 | 44 | @SuppressWarnings( "NonAsciiCharacters" ) |
| 45 | public class ApplicationActions { | |
| 45 | public final class ApplicationActions { | |
| 46 | 46 | private static final String STYLE_SEARCH = "search"; |
| 47 | 47 | |
| ... | ||
| 125 | 125 | private void file‿export( final ExportFormat format ) { |
| 126 | 126 | final var main = getMainPane(); |
| 127 | final var context = main.createProcessorContext(); | |
| 127 | final var context = main.createProcessorContext( format ); | |
| 128 | 128 | final var chain = createProcessors( context ); |
| 129 | 129 | final var editor = main.getActiveTextEditor(); |
| 19 | 19 | * and keyboard shortcuts. |
| 20 | 20 | */ |
| 21 | public class ApplicationBars { | |
| 21 | public final class ApplicationBars { | |
| 22 | 22 | |
| 23 | 23 | private static final Map<String, Action> sMap = new HashMap<>( 64 ); |
| ... | ||
| 119 | 119 | addAction( "view.preview", e -> actions.view‿preview() ), |
| 120 | 120 | SEPARATOR_ACTION, |
| 121 | addAction( "view.menubar", e -> actions.view‿menubar() ), | |
| 121 | 122 | addAction( "view.toolbar", e -> actions.view‿toolbar() ), |
| 122 | addAction( "view.statusbar", e -> actions.view‿statusbar() ), | |
| 123 | addAction( "view.menubar", e -> actions.view‿menubar() ) | |
| 123 | addAction( "view.statusbar", e -> actions.view‿statusbar() ) | |
| 124 | 124 | ), |
| 125 | 125 | createMenu( |
| 24 | 24 | * select files. |
| 25 | 25 | */ |
| 26 | public class FileChooserCommand { | |
| 26 | public final class FileChooserCommand { | |
| 27 | 27 | private static final String FILTER_EXTENSION_TITLES = |
| 28 | 28 | "Dialog.file.choose.filter"; |
| 9 | 9 | * operation, acting as a placeholder for line separators. |
| 10 | 10 | */ |
| 11 | public class SeparatorAction implements MenuAction { | |
| 11 | public final class SeparatorAction implements MenuAction { | |
| 12 | 12 | @Override |
| 13 | 13 | public MenuItem createMenuItem() { |
| 12 | 12 | |
| 13 | 13 | import java.time.LocalDateTime; |
| 14 | import java.util.Objects; | |
| 14 | 15 | import java.util.TreeSet; |
| 15 | 16 | import java.util.stream.Collectors; |
| ... | ||
| 35 | 36 | * Responsible for logging application issues to {@link TableView} entries. |
| 36 | 37 | */ |
| 37 | public class LogView extends Alert { | |
| 38 | public final class LogView extends Alert { | |
| 38 | 39 | /** |
| 39 | * Number of error messages to retain in the {@link TableView}, must be | |
| 40 | * Number of error messages to retain in the {@link TableView}; must be | |
| 40 | 41 | * greater than zero. |
| 41 | 42 | */ |
| ... | ||
| 84 | 85 | |
| 85 | 86 | private void log( final LogEntry logEntry ) { |
| 87 | // Exit early if the log already contains the message. The status bar will | |
| 88 | // remain current. | |
| 89 | if( mEntries.contains( logEntry ) ) { | |
| 90 | return; | |
| 91 | } | |
| 92 | ||
| 86 | 93 | mEntries.add( logEntry ); |
| 87 | 94 | |
| ... | ||
| 225 | 232 | |
| 226 | 233 | return sb.toString(); |
| 234 | } | |
| 235 | ||
| 236 | @Override | |
| 237 | public boolean equals( final Object o ) { | |
| 238 | if( this == o ) { return true; } | |
| 239 | if( o == null || getClass() != o.getClass() ) { return false; } | |
| 240 | ||
| 241 | return Objects.equals( mMessage.get(), ((LogEntry) o).mMessage.get() ); | |
| 242 | } | |
| 243 | ||
| 244 | @Override | |
| 245 | public int hashCode() { | |
| 246 | return mMessage != null ? mMessage.hashCode() : 0; | |
| 227 | 247 | } |
| 228 | 248 | } |
| 19 | 19 | * @param <T> The type of list to be cycled. |
| 20 | 20 | */ |
| 21 | public class CyclicIterator<T> implements ListIterator<T> { | |
| 21 | public final class CyclicIterator<T> implements ListIterator<T> { | |
| 22 | 22 | private final List<T> mList; |
| 23 | 23 |
| 23 | 23 | HTTP, |
| 24 | 24 | /** |
| 25 | * Denotes FTP. | |
| 26 | */ | |
| 27 | FTP, | |
| 28 | /** | |
| 25 | 29 | * Denotes Java archive file. |
| 26 | 30 | */ |
| ... | ||
| 41 | 45 | final var uri = new URI( resource ); |
| 42 | 46 | return uri.isAbsolute() |
| 43 | ? valueFrom( uri ) | |
| 44 | : valueFrom( new URL( resource ) ); | |
| 47 | ? valueFrom( uri ) | |
| 48 | : valueFrom( new URL( resource ) ); | |
| 45 | 49 | } catch( final Exception ex ) { |
| 46 | 50 | // Using double-slashes is a short-hand to instruct the browser to |
| 47 | 51 | // reference a resource using the parent URL's security model. This |
| 48 | 52 | // is known as a protocol-relative URL. |
| 49 | 53 | return resource.startsWith( "//" ) |
| 50 | ? HTTP | |
| 51 | : valueFrom( new File( resource ) ); | |
| 54 | ? HTTP | |
| 55 | : valueFrom( new File( resource ) ); | |
| 52 | 56 | } |
| 53 | 57 | } |
| ... | ||
| 121 | 125 | |
| 122 | 126 | /** |
| 123 | * Answers {@code true} if the given protocol is either HTTP or HTTPS. | |
| 127 | * Answers whether the given protocol is HTTP or HTTPS. | |
| 124 | 128 | * |
| 125 | 129 | * @return {@code true} the protocol is either HTTP or HTTPS. |
| 126 | 130 | */ |
| 127 | 131 | public boolean isHttp() { |
| 132 | return this == HTTP; | |
| 133 | } | |
| 134 | ||
| 135 | /** | |
| 136 | * Answers whether the given protocol is FTP. | |
| 137 | * | |
| 138 | * @return {@code true} the protocol is FTP. | |
| 139 | */ | |
| 140 | public boolean isFtp() { | |
| 128 | 141 | return this == HTTP; |
| 142 | } | |
| 143 | ||
| 144 | /** | |
| 145 | * Answers whether the given protocol represents a remote resource. | |
| 146 | * | |
| 147 | * @return {@code true} the protocol is HTTP(S) or FTP. | |
| 148 | */ | |
| 149 | public boolean isRemote() { | |
| 150 | return isHttp() || isFtp(); | |
| 129 | 151 | } |
| 130 | 152 | |
| 19 | 19 | * Responsible for finding file resources. |
| 20 | 20 | */ |
| 21 | public class ResourceWalker { | |
| 21 | public final class ResourceWalker { | |
| 22 | 22 | /** |
| 23 | 23 | * Globbing pattern to match font names. |
| 1 | .root { | |
| 2 | -fx-accent: #1e74c6; | |
| 3 | -fx-focus-color: -fx-accent; | |
| 4 | -fx-base: #373e43; | |
| 5 | -fx-control-inner-background: derive(-fx-base, 35%); | |
| 6 | -fx-control-inner-background-alt: -fx-control-inner-background ; | |
| 7 | } | |
| 8 | ||
| 9 | .label{ | |
| 10 | -fx-text-fill: lightgray; | |
| 11 | } | |
| 12 | ||
| 13 | .text-field { | |
| 14 | -fx-prompt-text-fill: gray; | |
| 15 | } | |
| 16 | ||
| 17 | .titulo{ | |
| 18 | -fx-font-weight: bold; | |
| 19 | -fx-font-size: 18px; | |
| 20 | } | |
| 21 | ||
| 22 | .button{ | |
| 23 | -fx-focus-traversable: false; | |
| 24 | } | |
| 25 | ||
| 26 | .button:hover{ | |
| 27 | -fx-text-fill: white; | |
| 28 | } | |
| 29 | ||
| 30 | .separator *.line { | |
| 31 | -fx-background-color: #3C3C3C; | |
| 32 | -fx-border-style: solid; | |
| 33 | -fx-border-width: 1px; | |
| 34 | } | |
| 35 | ||
| 36 | .scroll-bar{ | |
| 37 | -fx-background-color: derive(-fx-base,45%) | |
| 38 | } | |
| 39 | ||
| 40 | .button:default { | |
| 41 | -fx-base: -fx-accent ; | |
| 42 | } | |
| 43 | ||
| 44 | .table-view{ | |
| 45 | /*-fx-background-color: derive(-fx-base, 10%);*/ | |
| 46 | -fx-selection-bar-non-focused: derive(-fx-base, 50%); | |
| 47 | } | |
| 48 | ||
| 49 | .table-view .column-header .label{ | |
| 50 | -fx-alignment: CENTER_LEFT; | |
| 51 | -fx-font-weight: none; | |
| 52 | } | |
| 53 | ||
| 54 | .list-cell:even, | |
| 55 | .list-cell:odd, | |
| 56 | .table-row-cell:even, | |
| 57 | .table-row-cell:odd{ | |
| 58 | -fx-control-inner-background: derive(-fx-base, 15%); | |
| 59 | } | |
| 60 | ||
| 61 | .list-cell:empty, | |
| 62 | .table-row-cell:empty { | |
| 63 | -fx-background-color: transparent; | |
| 64 | } | |
| 65 | ||
| 66 | .list-cell, | |
| 67 | .table-row-cell{ | |
| 68 | -fx-border-color: transparent; | |
| 69 | -fx-table-cell-border-color:transparent; | |
| 70 | } | |
| 71 | ||
| 1 | 72 |
| 1 | /* | |
| 2 | * This is an adjustment of the original modena.css for a consistent dark theme. | |
| 3 | * Original modena.css here: https://gist.github.com/maxd/63691840fc372f22f470. | |
| 4 | */ | |
| 5 | ||
| 6 | /* Redefine base colors */ | |
| 7 | .root { | |
| 8 | -fx-base: rgb(50, 50, 50); | |
| 9 | -fx-background: rgb(50, 50, 50); | |
| 10 | ||
| 11 | /* make controls (buttons, thumb, etc.) slightly lighter */ | |
| 12 | -fx-color: derive(-fx-base, 10%); | |
| 13 | ||
| 14 | /* text fields and table rows background */ | |
| 15 | -fx-control-inner-background: rgb(20, 20, 20); | |
| 16 | /* version of -fx-control-inner-background for alternative rows */ | |
| 17 | -fx-control-inner-background-alt: derive(-fx-control-inner-background, 2.5%); | |
| 18 | ||
| 19 | /* text colors depending on background's brightness */ | |
| 20 | -fx-light-text-color: rgb(220, 220, 220); | |
| 21 | -fx-mid-text-color: rgb(100, 100, 100); | |
| 22 | -fx-dark-text-color: rgb(20, 20, 20); | |
| 23 | ||
| 24 | /* A bright blue for highlighting/accenting objects. For example: selected | |
| 25 | * text; selected items in menus, lists, trees, and tables; progress bars */ | |
| 26 | -fx-accent: rgb(0, 80, 100); | |
| 27 | ||
| 28 | /* color of non-focused yet selected elements */ | |
| 29 | -fx-selection-bar-non-focused: rgb(50, 50, 50); | |
| 30 | } | |
| 31 | ||
| 32 | /* Fix derived prompt color for text fields */ | |
| 33 | .text-input { | |
| 34 | -fx-prompt-text-fill: derive(-fx-control-inner-background, +50%); | |
| 35 | } | |
| 36 | ||
| 37 | /* Keep prompt invisible when focused (above color fix overrides it) */ | |
| 38 | .text-input:focused { | |
| 39 | -fx-prompt-text-fill: transparent; | |
| 40 | } | |
| 41 | ||
| 42 | /* Fix scroll bar buttons arrows colors */ | |
| 43 | .scroll-bar > .increment-button > .increment-arrow, | |
| 44 | .scroll-bar > .decrement-button > .decrement-arrow { | |
| 45 | -fx-background-color: -fx-mark-highlight-color, rgb(220, 220, 220); | |
| 46 | } | |
| 47 | ||
| 48 | .scroll-bar > .increment-button:hover > .increment-arrow, | |
| 49 | .scroll-bar > .decrement-button:hover > .decrement-arrow { | |
| 50 | -fx-background-color: -fx-mark-highlight-color, rgb(240, 240, 240); | |
| 51 | } | |
| 1 | 52 | |
| 53 | .scroll-bar > .increment-button:pressed > .increment-arrow, | |
| 54 | .scroll-bar > .decrement-button:pressed > .decrement-arrow { | |
| 55 | -fx-background-color: -fx-mark-highlight-color, rgb(255, 255, 255); | |
| 56 | } |
| 1 | /* ========================= | |
| 2 | * == JFX Controls == | |
| 3 | * ========================= | |
| 4 | */ | |
| 5 | .root { | |
| 6 | -fx-base: rgb(45, 45, 46); | |
| 7 | -fx-background: rgb(45, 45, 46); | |
| 8 | /* Brighten controls */ | |
| 9 | -fx-color: derive(-fx-base, -40%); | |
| 10 | /* Control background */ | |
| 11 | -fx-control-inner-background: rgb(46, 46, 47); | |
| 12 | /* Alternative control background (rows) */ | |
| 13 | -fx-control-inner-background-alt: derive(-fx-control-inner-background, 2.5%); | |
| 14 | /* Text colors */ | |
| 15 | -fx-light-text-color: rgb(220, 220, 220); | |
| 16 | -fx-mid-text-color: rgb(100, 100, 100); | |
| 17 | -fx-dark-text-color: rgb(20, 20, 20); | |
| 18 | /* Accent colors */ | |
| 19 | -fx-accent: rgb(51, 51, 52); | |
| 20 | -fx-focus-color: rgb(51, 51, 52); | |
| 21 | /* Non-focused-selected elements */ | |
| 22 | -fx-selection-bar-non-focused: rgb(45, 45, 46); | |
| 23 | } | |
| 24 | * { | |
| 25 | -fx-highlight-fill: rgba(0, 180, 255, 0.4); | |
| 26 | } | |
| 27 | /* Scroll */ | |
| 28 | .scroll-bar { | |
| 29 | -fx-background-color: rgb(61,61,62); | |
| 30 | } | |
| 31 | .scroll-bar .thumb { | |
| 32 | -fx-background-color: rgb(91,91,92); | |
| 33 | -fx-background-radius: 0; | |
| 34 | } | |
| 35 | .scroll-bar .thumb:hover, | |
| 36 | .scroll-bar .thumb:pressed { | |
| 37 | -fx-background-color: rgb(141,141,142); | |
| 38 | } | |
| 39 | .scroll-bar .increment-button .increment-arrow, | |
| 40 | .scroll-bar .decrement-button .decrement-arrow { | |
| 41 | -fx-background-color: rgb(200,200,200); | |
| 42 | } | |
| 43 | .corner { | |
| 44 | -fx-background-color: rgb(61,61,62); | |
| 45 | } | |
| 46 | /* Menu */ | |
| 47 | .menu-bar { | |
| 48 | -fx-background-color: rgb(45, 45, 48); | |
| 49 | } | |
| 50 | .menu { | |
| 51 | -fx-padding: 6 14 6 14; | |
| 52 | -fx-background-insets: -1; | |
| 53 | } | |
| 54 | .menu-item { | |
| 55 | -fx-padding: 5 11 5 11; | |
| 56 | -fx-background-insets: -1; | |
| 57 | } | |
| 58 | .menu:hover { | |
| 59 | -fx-background-color: rgb(61, 61, 62); | |
| 60 | } | |
| 61 | .context-menu, | |
| 62 | .menu:showing { | |
| 63 | -fx-background-color: rgb(27, 27, 28); | |
| 64 | -fx-border-insets: -1; | |
| 65 | -fx-border-width: 1; | |
| 66 | -fx-border-color: black; | |
| 67 | } | |
| 68 | .context-menu { | |
| 69 | -fx-min-width: 80px; | |
| 70 | -fx-background-insets: -1; | |
| 71 | -fx-border-insets: -1; | |
| 72 | -fx-border-width: 1; | |
| 73 | -fx-border-color: black; | |
| 74 | } | |
| 75 | .context-menu .menu-item:focused { | |
| 76 | -fx-background-color: rgb(61, 61, 62); | |
| 77 | } | |
| 78 | .context-menu-header { | |
| 79 | /* TODO: Find a way to disable hover coloring on the menu header */ | |
| 80 | -fx-opacity: 1.0; | |
| 81 | -fx-background-color: rgb(24, 50, 95); | |
| 82 | } | |
| 83 | .context-menu-header .label { | |
| 84 | -fx-opacity: 1.0; | |
| 85 | } | |
| 86 | ||
| 87 | /* Tabs */ | |
| 88 | .tab-pane { | |
| 89 | -fx-tab-min-width: 100px; | |
| 90 | } | |
| 91 | .tab-pane *.tab-header-background { | |
| 92 | -fx-background-color: rgb(29, 29, 31); | |
| 93 | -fx-border-width: 0 0 1 0; | |
| 94 | -fx-border-color: black; | |
| 95 | } | |
| 96 | .headers-region { | |
| 97 | -fx-background-color: rgb(75, 75, 76); | |
| 98 | } | |
| 99 | .tab { | |
| 100 | -fx-background-color: rgb(36,36,37); | |
| 101 | -fx-background-insets: 2 -1 -1 -1; | |
| 102 | -fx-background-radius: 0; | |
| 103 | -fx-padding: 2 2 1 2; | |
| 104 | -fx-border-insets: 0; | |
| 105 | -fx-border-width: 1 1 1 1; | |
| 106 | -fx-border-color: black; | |
| 107 | } | |
| 108 | .tab:selected { | |
| 109 | -fx-background-color: rgb(45, 45, 46); | |
| 110 | -fx-background-insets: 2 -1 -1 -1; | |
| 111 | -fx-padding: 2; | |
| 112 | -fx-border-insets: 0; | |
| 113 | -fx-border-width: 1 1 0 1; | |
| 114 | -fx-border-color: black; | |
| 115 | } | |
| 116 | .tab:selected .focus-indicator { | |
| 117 | -fx-border-color: transparent; | |
| 118 | } | |
| 119 | /* Table */ | |
| 120 | .table-view { | |
| 121 | -fx-selection-bar: rgb(50, 71, 77); | |
| 122 | -fx-selection-bar-non-focused: rgb(46, 56, 59); | |
| 123 | -fx-background-color: rgb(36,36,37); | |
| 124 | -fx-background-insets: 2 -1 -1 -1; | |
| 125 | -fx-background-radius: 0; | |
| 126 | -fx-padding: -1; | |
| 127 | -fx-border-width: 0 1 1 1; | |
| 128 | -fx-border-color: rgb(22, 22, 23); | |
| 129 | } | |
| 130 | .table-view .filler, | |
| 131 | .table-view .show-hide-columns-button, | |
| 132 | .column-overlay { | |
| 133 | -fx-background-color: transparent; | |
| 134 | } | |
| 135 | .column-header-background { | |
| 136 | -fx-background-color: rgb(36,36,37); | |
| 137 | -fx-background-insets: 2 -1 -1 -1; | |
| 138 | -fx-padding: -1; | |
| 139 | -fx-border-insets: 0; | |
| 140 | -fx-border-width: 0 1 0 1; | |
| 141 | -fx-border-color: rgb(22, 22, 23); | |
| 142 | } | |
| 143 | .column-header { | |
| 144 | -fx-background-color: rgb(45, 45, 46); | |
| 145 | -fx-background-insets: -1 -0 -1 0; | |
| 146 | -fx-padding: 2; | |
| 147 | -fx-border-insets: 1 -1 1 0; | |
| 148 | -fx-border-width: 1; | |
| 149 | -fx-border-color: rgb(22, 22, 23); | |
| 150 | } | |
| 151 | /* Splitpane */ | |
| 152 | .split-pane-divider { | |
| 153 | -fx-background-color: black; | |
| 154 | -fx-padding: 0; | |
| 155 | -fx-background-insets: -5; | |
| 156 | } | |
| 157 | /* Tree */ | |
| 158 | .tree-table-view, | |
| 159 | .tree-view { | |
| 160 | -fx-background-color: rgb(29, 29, 31); | |
| 161 | -fx-background-insets: 0; | |
| 162 | -fx-border-width: 0 1 0 0; | |
| 163 | -fx-border-color: black; | |
| 164 | } | |
| 165 | .tree-table-cell, | |
| 166 | .tree-cell { | |
| 167 | -fx-background-color: rgb(29, 29, 31); | |
| 168 | } | |
| 169 | .tree-cell:selected { | |
| 170 | -fx-background-color: rgb(44, 48, 55); | |
| 171 | } | |
| 172 | /* Buttons */ | |
| 173 | .box, | |
| 174 | .button, | |
| 175 | .combo-box, | |
| 176 | .slider .thumb { | |
| 177 | -fx-background-radius: 0; | |
| 178 | -fx-background-color: rgb(63, 63, 70); | |
| 179 | -fx-background-insets: 0; | |
| 180 | -fx-border-width: 1; | |
| 181 | -fx-border-color: rgb(85, 85, 85); | |
| 182 | } | |
| 183 | .check-box:hover .box, | |
| 184 | .button:hover, | |
| 185 | .combo-box:hover, | |
| 186 | .slider .thumb:hover { | |
| 187 | -fx-background-color: rgb(80, 80, 85); | |
| 188 | -fx-border-color: rgb(0, 122, 205); | |
| 189 | } | |
| 190 | .check-box:pressed .box, | |
| 191 | .button:pressed, | |
| 192 | .combo-box:pressed, | |
| 193 | .slider .thumb:pressed { | |
| 194 | -fx-background-color: rgb(0, 122, 205); | |
| 195 | -fx-border-color: rgb(0, 162, 245); | |
| 196 | } | |
| 197 | .combo-box:showing { | |
| 198 | -fx-background-color: rgb(27, 27, 28); | |
| 199 | -fx-border-width: 1 1 0 1; | |
| 200 | -fx-border-color: black; | |
| 201 | } | |
| 202 | .combo-box .combo-box-popup .list-cell { | |
| 203 | -fx-background-color: rgb(27, 27, 28); | |
| 204 | } | |
| 205 | .combo-box .combo-box-popup .list-cell:hover { | |
| 206 | -fx-background-color: rgb(61, 61, 62); | |
| 207 | } | |
| 208 | .combo-box .combo-box-popup .list-view { | |
| 209 | -fx-background-color: rgb(27, 27, 28); | |
| 210 | -fx-border-width: 0 1 1 1; | |
| 211 | -fx-border-color: black; | |
| 212 | } | |
| 213 | .hyperlink { | |
| 214 | -fx-text-fill: rgb(30, 132, 250); | |
| 215 | } | |
| 216 | hyperlink:visited { | |
| 217 | -fx-text-fill: rgb(98, 59, 217); | |
| 218 | } | |
| 219 | /* slider */ | |
| 220 | .slider .track { | |
| 221 | -fx-background-radius: 0; | |
| 222 | -fx-background-color: rgb(29, 29, 31); | |
| 223 | -fx-background-insets: 0; | |
| 224 | -fx-border-width: 1; | |
| 225 | -fx-border-color: rgb(65, 65, 65); | |
| 226 | } | |
| 227 | .slider .thumb { | |
| 228 | /* | |
| 229 | -fx-background-insets: 3; | |
| 230 | -fx-border-insets: 3; | |
| 231 | */ | |
| 232 | -fx-padding: 5; | |
| 233 | } | |
| 234 | .axis-tick-mark { | |
| 235 | -fx-stroke: rgb(100, 100, 100); | |
| 236 | } | |
| 237 | /* Text */ | |
| 238 | .text-area .content, | |
| 239 | .text-field { | |
| 240 | -fx-background-radius: 0; | |
| 241 | -fx-background-color: rgb(63, 63, 70); | |
| 242 | -fx-background-insets: 0; | |
| 243 | -fx-border-width: 1; | |
| 244 | -fx-border-color: rgb(85, 85, 85); | |
| 245 | } | |
| 246 | .text-area { | |
| 247 | -fx-background-radius: 0; | |
| 248 | -fx-background-color: rgb(63, 63, 70); | |
| 249 | -fx-background-insets: 0; | |
| 250 | -fx-border-width: 1; | |
| 251 | -fx-border-color: rgb(85, 85, 85); | |
| 252 | } | |
| 253 | .text-area .content { | |
| 254 | -fx-border-width: 0; | |
| 255 | } | |
| 256 | /* Popup */ | |
| 257 | .tooltip { | |
| 258 | -fx-background-radius: 0; | |
| 259 | -fx-background-color: rgb(40, 40, 42); | |
| 260 | -fx-background-insets: 0; | |
| 261 | -fx-border-width: 1; | |
| 262 | -fx-border-color: rgb(70, 70, 72); | |
| 263 | } | |
| 264 | /* ========================= | |
| 265 | * == Attach Elements == | |
| 266 | * ========================= | |
| 267 | */ | |
| 268 | .vm-view { | |
| 269 | -fx-border-width: 0 0 0 1; | |
| 270 | -fx-border-color: black; | |
| 271 | } | |
| 272 | .vm-buttons { | |
| 273 | -fx-padding: 1 0 1 0; | |
| 274 | } | |
| 275 | .vm-buttons .button { | |
| 276 | -fx-min-width: 140px; | |
| 277 | -fx-min-height: 48px; | |
| 278 | } | |
| 279 | .vm-icon { | |
| 280 | -fx-padding: 2 15 2 2; | |
| 281 | } | |
| 1 | 282 | |
| 283 | /* ========================= | |
| 284 | * == History Elements == | |
| 285 | * ========================= | |
| 286 | */ | |
| 287 | .hist-view { | |
| 288 | -fx-border-width: 0 0 0 1; | |
| 289 | -fx-border-color: black; | |
| 290 | } | |
| 291 | .hist-buttons { | |
| 292 | -fx-padding: 1 0 1 0; | |
| 293 | } | |
| 294 | .hist-buttons .button { | |
| 295 | -fx-min-width: 140px; | |
| 296 | -fx-min-height: 48px; | |
| 297 | } | |
| 298 | .hist-icon { | |
| 299 | -fx-padding: 2 13 2 2; | |
| 300 | } | |
| 301 | /* ========================= | |
| 302 | * == Other Elements == | |
| 303 | * ========================= | |
| 304 | */ | |
| 305 | .faint { | |
| 306 | -fx-text-fill: rgb(134, 134, 135); | |
| 307 | } | |
| 308 | .search-button { | |
| 309 | -fx-background-image: url('../icons/find-light.png'); | |
| 310 | } | |
| 311 | .search-field { | |
| 312 | -fx-prompt-text-fill: rgb(134, 134, 135); | |
| 313 | -fx-background-image: url('../icons/find-light.png'); | |
| 314 | -fx-background-color: rgb(39, 39, 41); | |
| 315 | -fx-border-width: 1; | |
| 316 | -fx-border-insets: 0 0 -1 -1; | |
| 317 | -fx-border-color: black; | |
| 318 | } | |
| 319 | .resource-selector { | |
| 320 | -fx-prompt-text-fill: rgb(134, 134, 135); | |
| 321 | -fx-background-color: rgb(39, 39, 41); | |
| 322 | -fx-border-color: rgb(39, 39, 41) black black rgb(39, 39, 41); | |
| 323 | -fx-border-insets: 0 0 0 -1; | |
| 324 | } | |
| 325 | .resource-selector:hover { | |
| 326 | -fx-border-width: 1; | |
| 327 | -fx-border-insets: 0; | |
| 328 | -fx-padding: 0 0 0 -1; | |
| 329 | } | |
| 330 | .resource-selector:showing { | |
| 331 | -fx-border-color: black; | |
| 332 | -fx-border-insets: 0; | |
| 333 | -fx-border-width: 1 1 0 1; | |
| 334 | -fx-padding: 0 0 1 -1; | |
| 335 | } | |
| 336 | /* Javadoc popup */ | |
| 337 | .drag-popup-wrapper { | |
| 338 | -fx-background-radius: 0; | |
| 339 | -fx-background-color: rgb(40, 40, 42); | |
| 340 | -fx-background-insets: 0; | |
| 341 | -fx-border-width: 1; | |
| 342 | -fx-border-color: rgb(95, 95, 95) | |
| 343 | } | |
| 344 | .drag-popup-wrapper .scroll-pane { | |
| 345 | -fx-background-insets: 0; | |
| 346 | -fx-border-width: 0; | |
| 347 | -fx-padding: 15; | |
| 348 | } | |
| 349 | .drag-popup-header { | |
| 350 | -fx-padding: 5; | |
| 351 | -fx-background-radius: 0; | |
| 352 | -fx-background-color: rgb(63, 63, 70); | |
| 353 | -fx-background-insets: 0; | |
| 354 | -fx-border-width: 0 0 1 0; | |
| 355 | -fx-border-color: rgb(95, 95, 95); | |
| 356 | } | |
| 357 | .update-header { | |
| 358 | -fx-padding: 5; | |
| 359 | -fx-background-color: rgb(32, 33, 35); | |
| 360 | -fx-border-width: 0 0 1 0; | |
| 361 | -fx-border-color: rgb(95, 95, 95); | |
| 362 | } | |
| 363 | .update-notes * { | |
| 364 | -fx-fill: rgb(220, 220, 220); | |
| 365 | } |
| 1 | /* $color-text: #dedce5; */ | |
| 2 | /* Sakura.css v1.3.1 | |
| 3 | * ================ | |
| 4 | * Minimal css theme. | |
| 5 | * Project: https://github.com/oxalorg/sakura/ | |
| 6 | */ | |
| 7 | /* Body */ | |
| 8 | html { | |
| 9 | font-size: 62.5%; | |
| 10 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; } | |
| 11 | ||
| 12 | body { | |
| 13 | font-size: 1.8rem; | |
| 14 | line-height: 1.618; | |
| 15 | max-width: 38em; | |
| 16 | margin: auto; | |
| 17 | color: #839496; | |
| 18 | background-color: #002b36; | |
| 19 | padding: 13px; } | |
| 20 | ||
| 21 | @media (max-width: 684px) { | |
| 22 | body { | |
| 23 | font-size: 1.53rem; } } | |
| 24 | ||
| 25 | @media (max-width: 382px) { | |
| 26 | body { | |
| 27 | font-size: 1.35rem; } } | |
| 28 | ||
| 29 | h1, h2, h3, h4, h5, h6 { | |
| 30 | line-height: 1.1; | |
| 31 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; | |
| 32 | font-weight: 700; | |
| 33 | margin-top: 3rem; | |
| 34 | margin-bottom: 1.5rem; | |
| 35 | overflow-wrap: break-word; | |
| 36 | word-wrap: break-word; | |
| 37 | -ms-word-break: break-all; | |
| 38 | word-break: break-word; } | |
| 39 | ||
| 40 | h1 { | |
| 41 | font-size: 2.35em; } | |
| 42 | ||
| 43 | h2 { | |
| 44 | font-size: 2.00em; } | |
| 45 | ||
| 46 | h3 { | |
| 47 | font-size: 1.75em; } | |
| 48 | ||
| 49 | h4 { | |
| 50 | font-size: 1.5em; } | |
| 51 | ||
| 52 | h5 { | |
| 53 | font-size: 1.25em; } | |
| 54 | ||
| 55 | h6 { | |
| 56 | font-size: 1em; } | |
| 57 | ||
| 58 | p { | |
| 59 | margin-top: 0px; | |
| 60 | margin-bottom: 2.5rem; } | |
| 61 | ||
| 62 | small, sub, sup { | |
| 63 | font-size: 75%; } | |
| 64 | ||
| 65 | hr { | |
| 66 | border-color: #2aa198; } | |
| 67 | ||
| 68 | a { | |
| 69 | text-decoration: none; | |
| 70 | color: #2aa198; } | |
| 71 | a:hover { | |
| 72 | color: #657b83; | |
| 73 | border-bottom: 2px solid #839496; } | |
| 74 | a:visited { | |
| 75 | color: #1f7972; } | |
| 76 | ||
| 77 | ul { | |
| 78 | padding-left: 1.4em; | |
| 79 | margin-top: 0px; | |
| 80 | margin-bottom: 2.5rem; } | |
| 81 | ||
| 82 | li { | |
| 83 | margin-bottom: 0.4em; } | |
| 84 | ||
| 85 | blockquote { | |
| 86 | margin-left: 0px; | |
| 87 | margin-right: 0px; | |
| 88 | padding-left: 1em; | |
| 89 | padding-top: 0.8em; | |
| 90 | padding-bottom: 0.8em; | |
| 91 | padding-right: 0.8em; | |
| 92 | border-left: 5px solid #2aa198; | |
| 93 | margin-bottom: 2.5rem; | |
| 94 | background-color: #073642; } | |
| 95 | ||
| 96 | blockquote p { | |
| 97 | margin-bottom: 0; } | |
| 98 | ||
| 99 | img, video { | |
| 100 | height: auto; | |
| 101 | max-width: 100%; | |
| 102 | margin-top: 0px; | |
| 103 | margin-bottom: 2.5rem; } | |
| 104 | ||
| 105 | /* Pre and Code */ | |
| 106 | pre { | |
| 107 | background-color: #073642; | |
| 108 | display: block; | |
| 109 | padding: 1em; | |
| 110 | overflow-x: auto; | |
| 111 | margin-top: 0px; | |
| 112 | margin-bottom: 2.5rem; } | |
| 113 | ||
| 114 | code { | |
| 115 | font-size: 0.9em; | |
| 116 | padding: 0 0.5em; | |
| 117 | background-color: #073642; | |
| 118 | white-space: pre-wrap; } | |
| 119 | ||
| 120 | pre > code { | |
| 121 | padding: 0; | |
| 122 | background-color: transparent; | |
| 123 | white-space: pre; } | |
| 124 | ||
| 125 | /* Tables */ | |
| 126 | table { | |
| 127 | text-align: justify; | |
| 128 | width: 100%; | |
| 129 | border-collapse: collapse; } | |
| 130 | ||
| 131 | td, th { | |
| 132 | padding: 0.5em; | |
| 133 | border-bottom: 1px solid #073642; } | |
| 134 | ||
| 135 | /* Buttons, forms and input */ | |
| 136 | input, textarea { | |
| 137 | border: 1px solid #839496; } | |
| 138 | input:focus, textarea:focus { | |
| 139 | border: 1px solid #2aa198; } | |
| 140 | ||
| 141 | textarea { | |
| 142 | width: 100%; } | |
| 143 | ||
| 144 | .button, button, input[type="submit"], input[type="reset"], input[type="button"] { | |
| 145 | display: inline-block; | |
| 146 | padding: 5px 10px; | |
| 147 | text-align: center; | |
| 148 | text-decoration: none; | |
| 149 | white-space: nowrap; | |
| 150 | background-color: #2aa198; | |
| 151 | color: #002b36; | |
| 152 | border-radius: 1px; | |
| 153 | border: 1px solid #2aa198; | |
| 154 | cursor: pointer; | |
| 155 | box-sizing: border-box; } | |
| 156 | .button[disabled], button[disabled], input[type="submit"][disabled], input[type="reset"][disabled], input[type="button"][disabled] { | |
| 157 | cursor: default; | |
| 158 | opacity: .5; } | |
| 159 | .button:focus:enabled, .button:hover:enabled, button:focus:enabled, button:hover:enabled, input[type="submit"]:focus:enabled, input[type="submit"]:hover:enabled, input[type="reset"]:focus:enabled, input[type="reset"]:hover:enabled, input[type="button"]:focus:enabled, input[type="button"]:hover:enabled { | |
| 160 | background-color: #657b83; | |
| 161 | border-color: #657b83; | |
| 162 | color: #002b36; | |
| 163 | outline: 0; } | |
| 164 | ||
| 165 | textarea, select, input { | |
| 166 | color: #839496; | |
| 167 | padding: 6px 10px; | |
| 168 | /* The 6px vertically centers text on FF, ignored by Webkit */ | |
| 169 | margin-bottom: 10px; | |
| 170 | background-color: #073642; | |
| 171 | border: 1px solid #073642; | |
| 172 | border-radius: 4px; | |
| 173 | box-shadow: none; | |
| 174 | box-sizing: border-box; } | |
| 175 | textarea:focus, select:focus, input:focus { | |
| 176 | border: 1px solid #2aa198; | |
| 177 | outline: 0; } | |
| 178 | ||
| 179 | input[type="checkbox"]:focus { | |
| 180 | outline: 1px dotted #2aa198; } | |
| 181 | ||
| 182 | label, legend, fieldset { | |
| 183 | display: block; | |
| 184 | margin-bottom: .5rem; | |
| 185 | font-weight: 600; } | |
| 1 | 186 |
| 1 | .root { | |
| 2 | -fx-base: rgb(50, 50, 50); | |
| 3 | -fx-background: rgb(50, 50, 50); | |
| 4 | -fx-control-inner-background: rgb(50, 50, 50); | |
| 5 | } | |
| 6 | ||
| 7 | .tab { | |
| 8 | -fx-background-color: linear-gradient(to top, -fx-base, derive(-fx-base,30%)); | |
| 9 | } | |
| 10 | ||
| 11 | .menu-bar { | |
| 12 | -fx-background-color: linear-gradient(to bottom, -fx-base, derive(-fx-base,30%)); | |
| 13 | } | |
| 14 | ||
| 15 | .tool-bar:horizontal { | |
| 16 | -fx-background-color: | |
| 17 | linear-gradient(to bottom, derive(-fx-base,+50%), derive(-fx-base,-40%), derive(-fx-base,-20%)); | |
| 18 | } | |
| 19 | ||
| 20 | .button { | |
| 21 | -fx-background-color: transparent; | |
| 22 | } | |
| 23 | ||
| 24 | .button:hover { | |
| 25 | -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; | |
| 26 | -fx-color: -fx-hover-base; | |
| 27 | } | |
| 28 | ||
| 29 | .table-view { | |
| 30 | -fx-table-cell-border-color:derive(-fx-base,+10%); | |
| 31 | -fx-table-header-border-color:derive(-fx-base,+20%); | |
| 32 | } | |
| 33 | ||
| 34 | .split-pane:horizontal > * > .split-pane-divider { | |
| 35 | -fx-border-color: transparent -fx-base transparent -fx-base; | |
| 36 | -fx-background-color: transparent, derive(-fx-base,20%); | |
| 37 | -fx-background-insets: 0, 0 1 0 1; | |
| 38 | } | |
| 39 | ||
| 40 | .my-gridpane { | |
| 41 | -fx-background-color: radial-gradient(radius 100%, derive(-fx-base,20%), derive(-fx-base,-20%)); | |
| 42 | } | |
| 43 | ||
| 44 | .separator-label { | |
| 45 | -fx-text-fill: orange; | |
| 46 | } | |
| 47 | ||
| 1 | 48 |
| 1 | /* Copyright 2020-2021 White Magic Software, Ltd. -- All rights reserved. */ | |
| 2 | package com.keenwrite; | |
| 3 | ||
| 4 | import javafx.embed.swing.JFXPanel; | |
| 5 | import org.junit.jupiter.api.extension.BeforeAllCallback; | |
| 6 | import org.junit.jupiter.api.extension.ExtensionContext; | |
| 7 | import org.testfx.osgi.service.TestFx; | |
| 8 | ||
| 9 | import java.util.concurrent.Semaphore; | |
| 10 | ||
| 11 | import static javafx.application.Platform.runLater; | |
| 12 | import static javax.swing.SwingUtilities.invokeLater; | |
| 13 | ||
| 14 | /** | |
| 15 | * Blocks all unit tests until JavaFX is ready. | |
| 16 | */ | |
| 17 | public class AwaitFxExtension implements BeforeAllCallback { | |
| 18 | /** | |
| 19 | * Prevent {@link RuntimeException} for internal graphics not initialized yet. | |
| 20 | * | |
| 21 | * @param context Provided by the {@link TestFx} framework. | |
| 22 | * @throws InterruptedException Could not acquire semaphore. | |
| 23 | */ | |
| 24 | @Override | |
| 25 | public void beforeAll( final ExtensionContext context ) | |
| 26 | throws InterruptedException { | |
| 27 | final var semaphore = new Semaphore( 0 ); | |
| 28 | ||
| 29 | invokeLater( () -> { | |
| 30 | // Prepare JavaFX toolkit and environment. | |
| 31 | new JFXPanel(); | |
| 32 | runLater( semaphore::release ); | |
| 33 | } ); | |
| 34 | ||
| 35 | semaphore.acquire(); | |
| 36 | } | |
| 37 | } | |
| 1 | 38 |
| 1 | 1 | package com.keenwrite.editors.markdown; |
| 2 | 2 | |
| 3 | import com.keenwrite.AwaitFxExtension; | |
| 3 | 4 | import com.keenwrite.preferences.Workspace; |
| 4 | 5 | import org.junit.jupiter.api.Test; |
| 5 | 6 | import org.junit.jupiter.api.extension.ExtendWith; |
| 6 | 7 | import org.testfx.framework.junit5.ApplicationExtension; |
| 7 | 8 | |
| 8 | 9 | import java.util.regex.Pattern; |
| 9 | 10 | |
| 10 | 11 | import static java.util.regex.Pattern.compile; |
| 12 | import static javafx.application.Platform.runLater; | |
| 11 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; |
| 12 | 14 | import static org.junit.jupiter.api.Assertions.assertTrue; |
| 13 | 15 | |
| 14 | @ExtendWith( ApplicationExtension.class ) | |
| 16 | @ExtendWith( {ApplicationExtension.class, AwaitFxExtension.class} ) | |
| 15 | 17 | public class MarkdownEditorTest { |
| 16 | 18 | private static final String[] WORDS = new String[]{ |
| ... | ||
| 52 | 54 | @Test |
| 53 | 55 | public void test_CaretWord_GetISO88591Word_WordSelected() { |
| 54 | final var editor = createMarkdownEditor(); | |
| 56 | runLater( () -> { | |
| 57 | final var editor = createMarkdownEditor(); | |
| 55 | 58 | |
| 56 | for( int i = 0; i < WORDS.length; i++ ) { | |
| 57 | final var word = WORDS[ i ]; | |
| 58 | final var len = word.length(); | |
| 59 | final var expected = REGEX.matcher( word ).replaceAll( "" ); | |
| 59 | for( int i = 0; i < WORDS.length; i++ ) { | |
| 60 | final var word = WORDS[ i ]; | |
| 61 | final var len = word.length(); | |
| 62 | final var expected = REGEX.matcher( word ).replaceAll( "" ); | |
| 60 | 63 | |
| 61 | for( int j = 0; j < len; j++ ) { | |
| 62 | editor.moveTo( offset( i ) + j ); | |
| 63 | final var actual = editor.getCaretWordText(); | |
| 64 | assertEquals( expected, actual ); | |
| 64 | for( int j = 0; j < len; j++ ) { | |
| 65 | editor.moveTo( offset( i ) + j ); | |
| 66 | final var actual = editor.getCaretWordText(); | |
| 67 | assertEquals( expected, actual ); | |
| 68 | } | |
| 65 | 69 | } |
| 66 | } | |
| 70 | } ); | |
| 67 | 71 | } |
| 68 | 72 | |
| 2 | 2 | package com.keenwrite.processors.markdown; |
| 3 | 3 | |
| 4 | import com.keenwrite.AwaitFxExtension; | |
| 5 | import com.keenwrite.Caret; | |
| 4 | 6 | import com.keenwrite.preferences.Workspace; |
| 7 | import com.keenwrite.preview.HtmlPreview; | |
| 8 | import com.keenwrite.processors.Processor; | |
| 9 | import com.keenwrite.processors.ProcessorContext; | |
| 5 | 10 | import com.keenwrite.processors.markdown.extensions.ImageLinkExtension; |
| 6 | 11 | import com.vladsch.flexmark.html.HtmlRenderer; |
| 7 | 12 | import com.vladsch.flexmark.parser.Parser; |
| 13 | import javafx.stage.Stage; | |
| 8 | 14 | import org.junit.jupiter.api.Test; |
| 9 | 15 | import org.junit.jupiter.api.extension.ExtendWith; |
| 10 | 16 | import org.testfx.framework.junit5.ApplicationExtension; |
| 17 | import org.testfx.framework.junit5.Start; | |
| 11 | 18 | |
| 12 | 19 | import java.io.File; |
| 13 | import java.net.URISyntaxException; | |
| 20 | import java.net.URI; | |
| 14 | 21 | import java.net.URL; |
| 15 | 22 | import java.nio.file.Path; |
| 16 | 23 | import java.nio.file.Paths; |
| 17 | 24 | import java.util.HashMap; |
| 18 | 25 | import java.util.List; |
| 19 | 26 | import java.util.Map; |
| 20 | 27 | |
| 28 | import static com.keenwrite.Constants.DOCUMENT_DEFAULT; | |
| 29 | import static com.keenwrite.ExportFormat.NONE; | |
| 21 | 30 | import static java.lang.String.format; |
| 31 | import static javafx.application.Platform.runLater; | |
| 22 | 32 | import static org.junit.jupiter.api.Assertions.assertEquals; |
| 23 | 33 | import static org.junit.jupiter.api.Assertions.assertNotNull; |
| 34 | import static org.testfx.util.WaitForAsyncUtils.waitForFxEvents; | |
| 24 | 35 | |
| 25 | 36 | /** |
| 26 | 37 | * Responsible for testing that linked images render into HTML according to |
| 27 | 38 | * the {@link ImageLinkExtension} rules. |
| 28 | 39 | */ |
| 29 | @ExtendWith( ApplicationExtension.class ) | |
| 40 | @ExtendWith( {ApplicationExtension.class, AwaitFxExtension.class} ) | |
| 30 | 41 | @SuppressWarnings( "SameParameterValue" ) |
| 31 | 42 | public class ImageLinkExtensionTest { |
| 43 | private static final Workspace sWorkspace = new Workspace( | |
| 44 | getResource( "workspace.xml" ) ); | |
| 32 | 45 | |
| 33 | 46 | private static final Map<String, String> IMAGES = new HashMap<>(); |
| ... | ||
| 57 | 70 | addUri( URI_PATH + ".png" ); |
| 58 | 71 | addUri( URI_PATH + ".jpg" ); |
| 59 | addUri( URI_PATH, URI_PATH + URI_IMAGE_EXT ); | |
| 72 | addUri( URI_PATH, getResource( URI_PATH + URI_IMAGE_EXT ) ); | |
| 60 | 73 | addUri( "//" + URI_WEB ); |
| 61 | 74 | addUri( "http://" + URI_WEB ); |
| 62 | 75 | addUri( "https://" + URI_WEB ); |
| 63 | 76 | } |
| 64 | 77 | |
| 65 | private static void addUri( final String uri ) { | |
| 66 | addUri( uri, uri ); | |
| 78 | private HtmlPreview mPreview; | |
| 79 | ||
| 80 | @Start | |
| 81 | @SuppressWarnings( "unused" ) | |
| 82 | private void start( final Stage stage ) { | |
| 83 | mPreview = new HtmlPreview( sWorkspace ); | |
| 67 | 84 | } |
| 68 | 85 | |
| 69 | private static void addUri( final String uriKey, final String uriValue ) { | |
| 70 | IMAGES.put( toMd( uriKey ), toHtml( uriValue ) ); | |
| 86 | private static void addUri( final String actualExpected ) { | |
| 87 | addUri( actualExpected, actualExpected ); | |
| 71 | 88 | } |
| 72 | 89 | |
| 73 | private static String toMd( final String file ) { | |
| 74 | return format( "", file ); | |
| 90 | private static void addUri( final String actual, final String expected ) { | |
| 91 | IMAGES.put( toMd( actual ), toHtml( expected ) ); | |
| 75 | 92 | } |
| 76 | 93 | |
| 77 | private static String toHtml( final String file ) { | |
| 94 | private static String toMd( final String resource ) { | |
| 95 | return format( "", resource ); | |
| 96 | } | |
| 97 | ||
| 98 | private static String toHtml( final String url ) { | |
| 78 | 99 | return format( |
| 79 | "<p><img src=\"%s\" alt=\"Tooltip\" title=\"Title\" /></p>\n", file ); | |
| 100 | "<p><img src=\"%s\" alt=\"Tooltip\" title=\"Title\" /></p>\n", url ); | |
| 80 | 101 | } |
| 81 | 102 | |
| 82 | 103 | /** |
| 83 | 104 | * Test that the key URIs present in the {@link #IMAGES} map are rendered |
| 84 | 105 | * as the value URIs present in the same map. |
| 85 | 106 | */ |
| 86 | 107 | @Test |
| 87 | void test_LocalImage_RelativePathWithExtension_ResolvedSuccessfully() | |
| 88 | throws URISyntaxException { | |
| 89 | final var workspace = new Workspace(); | |
| 90 | final var resource = getPathResource( URI_IMAGE ); | |
| 108 | void test_ImageLookup_RelativePathWithExtension_ResolvedSuccessfully() { | |
| 109 | final var resource = getResourcePath( URI_IMAGE ); | |
| 91 | 110 | final var imagePath = new File( URI_IMAGE ).toPath(); |
| 92 | 111 | final var subpaths = resource.getNameCount() - imagePath.getNameCount(); |
| 93 | 112 | final var subpath = resource.subpath( 0, subpaths ); |
| 94 | 113 | |
| 95 | 114 | // The root component isn't considered part of the path, so add it back. |
| 96 | final var path = resource.getRoot().resolve( subpath ); | |
| 97 | ||
| 98 | final var extension = ImageLinkExtension.create( path, workspace ); | |
| 115 | final var documentPath = Path.of( | |
| 116 | resource.getRoot().resolve( subpath ).toString(), | |
| 117 | DOCUMENT_DEFAULT.getName() ); | |
| 118 | final var context = createProcessorContext( documentPath ); | |
| 119 | final var extension = ImageLinkExtension.create( context ); | |
| 99 | 120 | final var extensions = List.of( extension ); |
| 100 | 121 | final var pBuilder = Parser.builder(); |
| 101 | 122 | final var hBuilder = HtmlRenderer.builder(); |
| 102 | 123 | final var parser = pBuilder.extensions( extensions ).build(); |
| 103 | 124 | final var renderer = hBuilder.extensions( extensions ).build(); |
| 104 | 125 | |
| 105 | 126 | assertNotNull( parser ); |
| 106 | 127 | assertNotNull( renderer ); |
| 107 | ||
| 108 | // Set a default (fallback) image directory search location. | |
| 109 | //getInstance().imagesDirectoryProperty().setValue( new File( "." ) ); | |
| 110 | 128 | |
| 111 | 129 | for( final var entry : IMAGES.entrySet() ) { |
| 112 | 130 | final var key = entry.getKey(); |
| 113 | 131 | final var node = parser.parse( key ); |
| 114 | 132 | final var expectedHtml = entry.getValue(); |
| 115 | final var actualHtml = renderer.render( node ); | |
| 133 | final var actualHtml = new StringBuilder( 128 ); | |
| 116 | 134 | |
| 117 | assertEquals( expectedHtml, actualHtml ); | |
| 135 | runLater( () -> actualHtml.append( renderer.render( node ) ) ); | |
| 136 | ||
| 137 | waitForFxEvents(); | |
| 138 | assertEquals( expectedHtml, actualHtml.toString() ); | |
| 118 | 139 | } |
| 119 | 140 | } |
| 120 | ||
| 121 | private Path getPathResource( final String path ) | |
| 122 | throws URISyntaxException { | |
| 123 | final var url = getResource( path ); | |
| 124 | assert url != null; | |
| 125 | 141 | |
| 126 | final var uri = url.toURI(); | |
| 127 | return Paths.get( uri ); | |
| 142 | /** | |
| 143 | * Creates a new {@link ProcessorContext} for the given file name path. | |
| 144 | * | |
| 145 | * @param documentPath Fully qualified path to the file name. | |
| 146 | * @return A context used for creating new {@link Processor} instances. | |
| 147 | */ | |
| 148 | private ProcessorContext createProcessorContext( final Path documentPath ) { | |
| 149 | return new ProcessorContext( | |
| 150 | mPreview, | |
| 151 | new HashMap<>(), | |
| 152 | documentPath, | |
| 153 | Caret.builder().build(), | |
| 154 | NONE, | |
| 155 | sWorkspace | |
| 156 | ); | |
| 128 | 157 | } |
| 129 | 158 | |
| 130 | private URL getResource( final String path ) { | |
| 131 | final var packagePath = getClass().getPackageName().replace( '.', '/' ); | |
| 159 | private static URL toUrl( final String path ) { | |
| 160 | final var clazz = ImageLinkExtensionTest.class; | |
| 161 | final var packagePath = clazz.getPackageName().replace( '.', '/' ); | |
| 132 | 162 | final var resourcePath = '/' + packagePath + '/' + path; |
| 133 | return getClass().getResource( resourcePath ); | |
| 163 | return clazz.getResource( resourcePath ); | |
| 164 | } | |
| 165 | ||
| 166 | private static URI toUri( final String path ) { | |
| 167 | try { | |
| 168 | return toUrl( path ).toURI(); | |
| 169 | } catch( final Exception ex ) { | |
| 170 | throw new RuntimeException( ex ); | |
| 171 | } | |
| 172 | } | |
| 173 | ||
| 174 | private static Path getResourcePath( final String path ) { | |
| 175 | return Paths.get( toUri( path ) ); | |
| 176 | } | |
| 177 | ||
| 178 | private static String getResource( final String path ) { | |
| 179 | return toUri( path ).toString(); | |
| 134 | 180 | } |
| 135 | 181 | } |
| 1 | <?xml version="1.0" encoding="UTF-8"?> | |
| 2 | <keenwrite> | |
| 3 | <workspace> | |
| 4 | <images> | |
| 5 | <order>svg pdf png jpg tiff</order> | |
| 6 | <dir></dir> | |
| 7 | </images> | |
| 8 | </workspace> | |
| 9 | </keenwrite> | |
| 1 | 10 |