Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
A .classpath
1
<?xml version="1.0" encoding="UTF-8"?>
2
<classpath>
3
	<classpathentry kind="src" path="src/main/java">
4
		<attributes>
5
			<attribute name="FROM_GRADLE_MODEL" value="true"/>
6
		</attributes>
7
	</classpathentry>
8
	<classpathentry kind="src" path="src/main/resources">
9
		<attributes>
10
			<attribute name="FROM_GRADLE_MODEL" value="true"/>
11
		</attributes>
12
	</classpathentry>
13
	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
14
		<accessrules>
15
			<accessrule kind="accessible" pattern="javafx/**"/>
16
		</accessrules>
17
	</classpathentry>
18
	<classpathentry exported="true" kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
19
	<classpathentry kind="output" path="bin"/>
20
</classpath>
121
A .gitattributes
1
# always use LF line endings
2
3
# ALL FILES:
4
#   Normalize line endings to LF on checkin and
5
#   prevent conversion to CRLF when the file is checked out.
6
7
* text eol=lf
8
9
10
# BINARY FILES:
11
#   Disable line ending normalize on checkin.
12
13
*.gif binary
14
*.jar binary
15
*.png binary
16
*.zip binary
117
A .gitignore
1
/bin/
2
/build/
3
/.gradle/
4
/gradle/
5
/.nb-gradle
6
/private
7
.nb-gradle-properties
8
scrivenvar.pro
19
A .project
1
<?xml version="1.0" encoding="UTF-8"?>
2
<projectDescription>
3
	<name>Markdown Writer FX</name>
4
	<comment></comment>
5
	<projects>
6
	</projects>
7
	<buildSpec>
8
		<buildCommand>
9
			<name>org.eclipse.jdt.core.javabuilder</name>
10
			<arguments>
11
			</arguments>
12
		</buildCommand>
13
		<buildCommand>
14
			<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
15
			<arguments>
16
			</arguments>
17
		</buildCommand>
18
	</buildSpec>
19
	<natures>
20
		<nature>org.eclipse.jdt.core.javanature</nature>
21
		<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
22
	</natures>
23
	<filteredResources>
24
		<filter>
25
			<id>1438449113801</id>
26
			<name></name>
27
			<type>26</type>
28
			<matcher>
29
				<id>org.eclipse.ui.ide.multiFilter</id>
30
				<arguments>1.0-projectRelativePath-matches-false-false-build</arguments>
31
			</matcher>
32
		</filter>
33
		<filter>
34
			<id>1438449113801</id>
35
			<name></name>
36
			<type>26</type>
37
			<matcher>
38
				<id>org.eclipse.ui.ide.multiFilter</id>
39
				<arguments>1.0-projectRelativePath-matches-false-false-.gradle</arguments>
40
			</matcher>
41
		</filter>
42
	</filteredResources>
43
</projectDescription>
144
A .settings/org.eclipse.buildship.core.prefs
1
GRADLE_BUILD_COMMANDS=org.eclipse.jdt.core.javabuilder
2
GRADLE_NATURES=org.eclipse.jdt.core.javanature
3
build.commands=org.eclipse.jdt.core.javabuilder
4
connection.arguments=
5
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
6
connection.gradle.user.home=null
7
connection.java.home=null
8
connection.jvm.arguments=
9
connection.project.dir=
10
derived.resources=.gradle,build
11
eclipse.preferences.version=1
12
natures=org.eclipse.jdt.core.javanature
13
project.path=\:
114
A .settings/org.eclipse.core.runtime.prefs
1
eclipse.preferences.version=1
2
line.separator=\n
13
A .settings/org.eclipse.jdt.core.prefs
1
eclipse.preferences.version=1
2
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
3
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
4
org.eclipse.jdt.core.compiler.compliance=1.8
5
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
6
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
7
org.eclipse.jdt.core.compiler.source=1.8
18
A .settings/org.eclipse.jdt.ui.prefs
1
eclipse.preferences.version=1
2
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
3
org.eclipse.jdt.ui.ignorelowercasenames=true
4
org.eclipse.jdt.ui.importorder=java;javafx;javax;org;com;
5
org.eclipse.jdt.ui.ondemandthreshold=99
6
org.eclipse.jdt.ui.staticondemandthreshold=99
7
sp_cleanup.add_default_serial_version_id=true
8
sp_cleanup.add_generated_serial_version_id=false
9
sp_cleanup.add_missing_annotations=true
10
sp_cleanup.add_missing_deprecated_annotations=true
11
sp_cleanup.add_missing_methods=false
12
sp_cleanup.add_missing_nls_tags=false
13
sp_cleanup.add_missing_override_annotations=true
14
sp_cleanup.add_missing_override_annotations_interface_methods=true
15
sp_cleanup.add_serial_version_id=false
16
sp_cleanup.always_use_blocks=true
17
sp_cleanup.always_use_parentheses_in_expressions=false
18
sp_cleanup.always_use_this_for_non_static_field_access=false
19
sp_cleanup.always_use_this_for_non_static_method_access=false
20
sp_cleanup.convert_functional_interfaces=false
21
sp_cleanup.convert_to_enhanced_for_loop=false
22
sp_cleanup.correct_indentation=false
23
sp_cleanup.format_source_code=false
24
sp_cleanup.format_source_code_changes_only=false
25
sp_cleanup.insert_inferred_type_arguments=false
26
sp_cleanup.make_local_variable_final=false
27
sp_cleanup.make_parameters_final=false
28
sp_cleanup.make_private_fields_final=true
29
sp_cleanup.make_type_abstract_if_missing_method=false
30
sp_cleanup.make_variable_declarations_final=true
31
sp_cleanup.never_use_blocks=false
32
sp_cleanup.never_use_parentheses_in_expressions=true
33
sp_cleanup.on_save_use_additional_actions=true
34
sp_cleanup.organize_imports=false
35
sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
36
sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
37
sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
38
sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
39
sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
40
sp_cleanup.remove_private_constructors=true
41
sp_cleanup.remove_redundant_type_arguments=true
42
sp_cleanup.remove_trailing_whitespaces=true
43
sp_cleanup.remove_trailing_whitespaces_all=true
44
sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
45
sp_cleanup.remove_unnecessary_casts=true
46
sp_cleanup.remove_unnecessary_nls_tags=false
47
sp_cleanup.remove_unused_imports=true
48
sp_cleanup.remove_unused_local_variables=false
49
sp_cleanup.remove_unused_private_fields=true
50
sp_cleanup.remove_unused_private_members=false
51
sp_cleanup.remove_unused_private_methods=true
52
sp_cleanup.remove_unused_private_types=true
53
sp_cleanup.sort_members=false
54
sp_cleanup.sort_members_all=false
55
sp_cleanup.use_anonymous_class_creation=false
56
sp_cleanup.use_blocks=false
57
sp_cleanup.use_blocks_only_for_return_and_throw=false
58
sp_cleanup.use_lambda=true
59
sp_cleanup.use_parentheses_in_expressions=false
60
sp_cleanup.use_this_for_non_static_field_access=false
61
sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
62
sp_cleanup.use_this_for_non_static_method_access=false
63
sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
64
sp_cleanup.use_type_arguments=false
165
A .travis.yml
1
language: java
2
3
jdk:
4
- oraclejdk8
5
6
# enable Java 8u45+, see https://github.com/travis-ci/travis-ci/issues/4042
7
addons:
8
  apt:
9
    packages:
10
      - oracle-java8-installer
11
os:
12
  - linux
13
14
# run in container
15
sudo: false
116
A CREDITS.md
1
Credits
2
===
13
4
  * Dave Jarvis: [Scrivenvar](https://github.com/DaveJarvis/scrivenvar/)
5
  * Karl Tauber: [Markdown Writer FX](https://github.com/JFormDesigner/markdown-writer-fx)
6
  * Tomas Mikula: [RichTextFX](https://github.com/TomasMikula/RichTextFX), [ReactFX](https://github.com/TomasMikula/ReactFX), [WellBehavedFX](https://github.com/TomasMikula/WellBehavedFX), [Flowless](https://github.com/TomasMikula/Flowless), and [UndoFX](https://github.com/TomasMikula/UndoFX)
7
  * Mikael Grev: [MigLayout](http://www.miglayout.com/)
8
  * Tom Eugelink: [MigPane](https://github.com/mikaelgrev/miglayout/blob/master/javafx/src/main/java/org/tbee/javafx/scene/layout/fxml/MigPane.java)
9
  * Vladimir Schneider: [flexmark](https://website.com)
10
  * Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
11
  * Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet)
12
 * David Croft, [File Preferences](https://github.com/eis/simple-suomi24-java-client/tree/master/src/main/java/net/infotrek/util/prefs)
A LICENSE
1
Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without
5
modification, are permitted provided that the following conditions are met:
6
7
* Redistributions of source code must retain the above copyright
8
  notice, this list of conditions and the following disclaimer.
9
10
* Redistributions in binary form must reproduce the above copyright
11
  notice, this list of conditions and the following disclaimer in the
12
  documentation and/or other materials provided with the distribution.
13
14
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
125
A README.md
1
![Logo](images/logo64.png)
2
3
$application.title$
4
===
5
6
Word processing with variables.
7
8
Requirements
9
---
10
[Java 8u40 or greater](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html).
11
12
Quick Start
13
---
14
Complete the following steps to run the application:
15
16
1. [Download](https://github.com/DaveJarvis/scrivenvar/releases) `scrivenvar.jar`.
17
1. Double-click `scrivenvar.jar` to start the application.
18
19
Features
20
---
21
* R integration
22
* User-defined variables, interpolated
23
* Real-time preview with variable substitution
24
* Auto-complete variable names based on variable values
25
* XML document transformation using XSLT2
26
* Platform independent (Windows, Linux, MacOS)
27
28
Future Features
29
---
30
* Spell check
31
* Search and replace using variables
32
* Re-organize variable names
33
34
Screenshot
35
---
36
37
![Screenshot](images/screenshot.png)
38
39
License
40
---
41
This software is licensed under the [BSD 2-Clause License](LICENSE).
142
A Scrivenvar.jfdproj
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project>
3
  <entry key="classpath1" value="bin" />
4
  <entry key="classpath2" value="lib/fontawesomefx-8.9.jar" />
5
  <entry key="sourcefolders1" value="src/main/java" />
6
  <entry key="sourcefolders2" value="src/main/resources" />
7
  <node name="designer">
8
    <entry key="_i18nJavaSettingsEnabled" value="true" />
9
    <entry key="_i18nSettingsEnabled" value="true" />
10
    <entry key="i18n.externalizeexcludes1" value="com.scrivenvar.controls.EscapeTextField#escapeCharacters" />
11
    <entry key="i18n.externalizeexcludes2" value="com.scrivenvar.controls.WebHyperlink#uri" />
12
    <entry key="i18n.javagetstringformat" value="Messages.get(${key})" />
13
  </node>
14
  <node name="javacodegenerator">
15
    <entry key="_codeStyleSettingsEnabled" value="true" />
16
    <entry key="_generalSettingsEnabled" value="true" />
17
    <entry key="explicitimports" value="true" />
18
    <entry key="lineseparator" value="\n" />
19
  </node>
20
</project>
121
A USAGE.md
1
# Introduction
2
3
This document describes how to write documentation (technical or otherwise) using a master copy for generating a variety of output formats, such as: HTML pages, PDFs, and EPUBs. What's more, the document provides an overview of how to use variables and--for the unintimidated--leverage the power of R, a programming language.
4
5
# Software Requirements
6
7
Install Java, ConTeXt, Pandoc, R, and Lib V8. Then install the R packages knitr, yaml, and devtools, and pluralize by running the following commands:
8
9
    sudo su -
10
    apt-get install default-jre
11
    apt-get install context
12
    apt-get install pandoc
13
    apt-get install r
14
    apt-get install libv8-dev
15
    r
16
    url <- "http://cran.us.r-project.org"
17
    install.packages('knitr', repos=url)
18
    install.packages('yaml', repos=url)
19
    install.packages('devtools', repos=url)
20
    devtools::install_github("hrbrmstr/pluralize")
21
22
To exit R, press `Ctrl+d` or type `q()` followed by pressing `Enter`.
23
24
The required software packages are installed.
25
26
# Markdown
27
28
|Table|Table|
29
|---|---|
30
|Data|Data|
131
A _config.yaml
1
application:
2
  title: Scrivenvar
3
14
A build.gradle
1
apply plugin: 'java'
2
apply plugin: 'java-library-distribution'
3
apply plugin: 'application'
4
5
repositories {
6
  jcenter()
7
}
8
9
compileJava {
10
  options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
11
}
12
13
dependencies {
14
  compile 'org.controlsfx:controlsfx:8.40.12'
15
  compile 'org.fxmisc.richtext:richtextfx:0.7-M3'
16
  compile 'com.miglayout:miglayout-javafx:5.0'
17
  compile 'de.jensd:fontawesomefx-fontawesome:4.5.0'
18
  compile 'org.ahocorasick:ahocorasick:0.3.0'
19
  compile 'com.vladsch.flexmark:flexmark:0.9.0'
20
  compile 'com.vladsch.flexmark:flexmark-ext-gfm-tables:0.9.0'
21
  compile 'com.vladsch.flexmark:flexmark-ext-superscript:0.9.0'
22
  compile 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.9.0'
23
  compile 'com.fasterxml.jackson.core:jackson-core:2.8.4'
24
  compile 'com.fasterxml.jackson.core:jackson-databind:2.8.4'
25
  compile 'com.fasterxml.jackson.core:jackson-annotations:2.8.4'
26
  compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.8.4'
27
  compile 'org.yaml:snakeyaml:1.17'
28
  compile 'com.ximpleware:vtd-xml:2.13'
29
  compile 'net.sf.saxon:Saxon-HE:9.7.0-14'
30
  compile 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
31
  compile 'org.apache.commons:commons-configuration2:2.1'
32
  compile files('libs/renjin-script-engine-0.8.2320-jar-with-dependencies.jar')
33
}
34
35
version = '1.2.5'
36
applicationName = 'scrivenvar'
37
mainClassName = 'com.scrivenvar.Main'
38
sourceCompatibility = JavaVersion.VERSION_1_8
39
40
jar {
41
  baseName = applicationName
42
  archiveName = "${applicationName}.jar"
43
  
44
  doFirst {
45
    from {
46
      configurations.compile.collect { it.isDirectory() ? it : zipTree(it) }
47
    }
48
  }
49
50
  // Remove digital signature files to ensure an executable JAR file.
51
  exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA' 
52
53
  manifest {
54
    attributes 'Main-Class': mainClassName
55
    attributes 'Class-Path': configurations.compile.collect {
56
     'libs/' + it.getName()
57
    }.join(' ')
58
  }
59
}
60
61
distributions {
62
  main {
63
    baseName = applicationName
64
    contents {
65
      from { ['LICENSE', 'README.md'] }
66
      into( 'images' ) {
67
        from { 'images' }
68
      }
69
    }
70
  }
71
}
172
A executable.txt
11
2
https://github.com/bmuschko/gradle-izpack-plugin
3
  http://izpack.org/
4
5
https://github.com/TheBoegl/gradle-launch4j
6
  http://launch4j.sourceforge.net/
7
8
https://github.com/FibreFoX/javafx-gradle-plugin
9
  https://docs.oracle.com/javase/8/docs/technotes/guides/deploy/self-contained-packaging.html#A1324980
10
11
http://www.jwrapper.com/java-to-exe-free.html
12
13
https://github.com/libgdx/packr
14
15
A gradle.properties
11
A images/logo64.png
Binary file
A images/screenshot.png
Binary file
A libs/renjin-script-engine-0.8.2320-jar-with-dependencies.jar
Binary file
A settings.gradle
11
A src/main/java/com/scrivenvar/AbstractFileFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
31
import com.scrivenvar.predicates.files.FileTypePredicate;
32
import com.scrivenvar.service.Settings;
33
import java.nio.file.Path;
34
import java.util.Iterator;
35
import java.util.List;
36
37
/**
38
 * Provides common behaviours for factories that instantiate classes based on
39
 * file type.
40
 *
41
 * @author White Magic Software, Ltd.
42
 */
43
public class AbstractFileFactory {
44
45
  private final Settings settings = Services.load( Settings.class );
46
47
  /**
48
   * Determines the file type from the path extension. This should only be
49
   * called when it is known that the file type won't be a definition file
50
   * (e.g., YAML or other definition source), but rather an editable file
51
   * (e.g., Markdown, XML, etc.).
52
   *
53
   * @param path The path with a file name extension.
54
   *
55
   * @return The FileType for the given path.
56
   */
57
  public FileType lookup( final Path path ) {
58
    return lookup( path, GLOB_PREFIX_FILE );
59
  }
60
61
  /**
62
   * Creates a file type that corresponds to the given path.
63
   *
64
   * @param path Reference to a variable definition file.
65
   * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
66
   *
67
   * @return The file type that corresponds to the given path.
68
   */
69
  protected FileType lookup( final Path path, final String prefix ) {
70
    final Settings properties = getSettings();
71
    final Iterator<String> keys = properties.getKeys( prefix );
72
73
    boolean found = false;
74
    FileType fileType = null;
75
76
    while( keys.hasNext() && !found ) {
77
      final String key = keys.next();
78
      final List<String> patterns = properties.getStringSettingList( key );
79
      final FileTypePredicate predicate = new FileTypePredicate( patterns );
80
81
      if( found = predicate.test( path.toFile() ) ) {
82
        // Remove the EXTENSIONS_PREFIX to get the filename extension mapped
83
        // to a standard name (as defined in the settings.properties file).
84
        final String suffix = key.replace( prefix + ".", "" );
85
        fileType = FileType.from( suffix );
86
      }
87
    }
88
89
    return fileType;
90
  }
91
92
  /**
93
   * Throws IllegalArgumentException because the given path could not be
94
   * recognized.
95
   *
96
   * @param type The detected path type (protocol, file extension, etc.).
97
   * @param path The path to a source of definitions.
98
   */
99
  protected void unknownFileType( final String type, final String path ) {
100
    throw new IllegalArgumentException(
101
      "Unknown type '" + type + "' for '" + path + "'."
102
    );
103
  }
104
105
  /**
106
   * Throws IllegalArgumentException because the extension for the given path
107
   * could not be recognized.
108
   *
109
   * @param path The path to a file that could not be loaded.
110
   */
111
  protected void unknownExtension( final Path path ) {
112
    throw new IllegalArgumentException(
113
      "Unknown extension for '" + path + "'."
114
    );
115
  }
116
117
  /**
118
   * Return the singleton Settings instance.
119
   *
120
   * @return The settings for
121
   */
122
  private Settings getSettings() {
123
    return this.settings;
124
  }
125
}
1126
A src/main/java/com/scrivenvar/AbstractPane.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.service.Options;
31
import java.util.prefs.Preferences;
32
import org.tbee.javafx.scene.layout.fxml.MigPane;
33
34
/**
35
 * Provides options to all subclasses.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public abstract class AbstractPane extends MigPane {
40
41
  private final Options options = Services.load( Options.class );
42
43
  protected Options getOptions() {
44
    return this.options;
45
  }
46
  
47
  protected Preferences getState() {
48
    return getOptions().getState();
49
  }
50
}
151
A src/main/java/com/scrivenvar/Constants.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.service.Settings;
31
import java.util.Collection;
32
33
/**
34
 * @author White Magic Software, Ltd.
35
 */
36
public class Constants {
37
38
  private static final Settings SETTINGS = Services.load( Settings.class );
39
40
  /**
41
   * Prevent instantiation, deliberately.
42
   */
43
  private Constants() {
44
  }
45
46
  private static String get( final String key ) {
47
    return SETTINGS.getSetting( key, "" );
48
  }
49
50
  private static int get( final String key, final int defaultValue ) {
51
    return SETTINGS.getSetting( key, defaultValue );
52
  }
53
54
  private static Collection<String> getStringSettingList( final String key ) {
55
    return SETTINGS.getStringSettingList( key );
56
  }
57
58
  // Bootstrapping...
59
  public static final String SETTINGS_NAME = "/com/scrivenvar/settings.properties";
60
61
  public static final String APP_TITLE = get( "application.title" );
62
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
63
  // Prevent double events when updating files on Linux (save and timestamp).
64
  public static final int APP_WATCHDOG_TIMEOUT = get( "application.watchdog.timeout", 100 );
65
66
  public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
67
  public static final String STYLESHEET_MARKDOWN = get( "file.stylesheet.markdown" );
68
  public static final String STYLESHEET_PREVIEW = get( "file.stylesheet.preview" );
69
  public static final String STYLESHEET_XML = get( "file.stylesheet.xml" );
70
71
  public static final String FILE_LOGO_16 = get( "file.logo.16" );
72
  public static final String FILE_LOGO_32 = get( "file.logo.32" );
73
  public static final String FILE_LOGO_128 = get( "file.logo.128" );
74
  public static final String FILE_LOGO_256 = get( "file.logo.256" );
75
  public static final String FILE_LOGO_512 = get( "file.logo.512" );
76
77
  public static final String CARET_POSITION_BASE = get( "caret.token.base" );
78
  public static final String CARET_POSITION_MD = get( "caret.token.markdown" );
79
  public static final String CARET_POSITION_HTML = get( "caret.token.html" );
80
81
  public static final String PREFS_ROOT = get( "preferences.root" );
82
  public static final String PREFS_STATE = get( "preferences.root.state" );
83
  public static final String PREFS_OPTIONS = get( "preferences.root.options" );
84
85
  // Refer to filename extension settings in the configuration file. Do not
86
  // terminate these prefixes with a period.
87
  public static final String GLOB_PREFIX_FILE = "file.ext";
88
  public static final String GLOB_PREFIX_DEFINITION = "definition." + GLOB_PREFIX_FILE;
89
90
  public static final Collection<String> GLOB_DEFINITION_EXTENSIONS
91
    = getStringSettingList( GLOB_PREFIX_FILE + ".definition" );
92
93
  // Different definition source protocols.
94
  public static final String DEFINITION_PROTOCOL_UNKNOWN = "unknown";
95
  public static final String DEFINITION_PROTOCOL_FILE = "file";
96
97
  // Takes two parameters: line number and column number.
98
  public static final String STATUS_BAR_LINE = "Main.statusbar.line";
99
  // "OK" text
100
  public static final String STATUS_BAR_DEFAULT = get( "Main.statusbar.state.default" );
101
  public static final String STATUS_PARSE_ERROR = "Main.statusbar.parse.error";
102
103
  // Persistent storage settings.
104
  
105
  /**
106
   * Location of the definition source file.
107
   */
108
  public static final String PERSIST_DEFINITION_SOURCE = "definitionSource";
109
  
110
  
111
  /**
112
   * Content of the R startup script.
113
   */
114
  public static final String PERSIST_R_STARTUP = "rStartup";
115
}
1116
A src/main/java/com/scrivenvar/FileEditorTab.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * Redistribution and use in source and binary forms, with or without
5
 * modification, are permitted provided that the following conditions are met:
6
 *
7
 *  o Redistributions of source code must retain the above copyright
8
 *    notice, this list of conditions and the following disclaimer.
9
 *
10
 *  o Redistributions in binary form must reproduce the above copyright
11
 *    notice, this list of conditions and the following disclaimer in the
12
 *    documentation and/or other materials provided with the distribution.
13
 *
14
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
 */
26
package com.scrivenvar;
27
28
import com.scrivenvar.editors.EditorPane;
29
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
30
import com.scrivenvar.service.events.Notification;
31
import com.scrivenvar.service.events.Notifier;
32
import java.nio.charset.Charset;
33
import static java.nio.charset.StandardCharsets.UTF_8;
34
import java.nio.file.Files;
35
import java.nio.file.Path;
36
import static java.util.Locale.ENGLISH;
37
import java.util.function.Consumer;
38
import javafx.application.Platform;
39
import javafx.beans.binding.Bindings;
40
import javafx.beans.property.BooleanProperty;
41
import javafx.beans.property.ReadOnlyBooleanProperty;
42
import javafx.beans.property.ReadOnlyBooleanWrapper;
43
import javafx.beans.property.SimpleBooleanProperty;
44
import javafx.beans.value.ChangeListener;
45
import javafx.beans.value.ObservableValue;
46
import javafx.event.Event;
47
import javafx.scene.Node;
48
import javafx.scene.Scene;
49
import javafx.scene.control.Tab;
50
import javafx.scene.control.Tooltip;
51
import javafx.scene.input.InputEvent;
52
import javafx.scene.text.Text;
53
import javafx.stage.Window;
54
import org.fxmisc.richtext.StyleClassedTextArea;
55
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
56
import org.fxmisc.richtext.model.TwoDimensional.Position;
57
import org.fxmisc.undo.UndoManager;
58
import org.fxmisc.wellbehaved.event.EventPattern;
59
import org.fxmisc.wellbehaved.event.InputMap;
60
import org.mozilla.universalchardet.UniversalDetector;
61
62
/**
63
 * Editor for a single file.
64
 *
65
 * @author Karl Tauber and White Magic Software, Ltd.
66
 */
67
public final class FileEditorTab extends Tab {
68
69
  private final Notifier alertService = Services.load( Notifier.class );
70
  private EditorPane editorPane;
71
72
  /**
73
   * Character encoding used by the file (or default encoding if none found).
74
   */
75
  private Charset encoding;
76
77
  private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper();
78
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
79
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
80
81
  private Path path;
82
83
  FileEditorTab( final Path path ) {
84
    setPath( path );
85
86
    this.modified.addListener( (observable, oldPath, newPath) -> updateTab() );
87
88
    setOnSelectionChanged( e -> {
89
      if( isSelected() ) {
90
        Platform.runLater( () -> activated() );
91
      }
92
    } );
93
  }
94
95
  private void updateTab() {
96
    setText( getTabTitle() );
97
    setGraphic( getModifiedMark() );
98
    setTooltip( getTabTooltip() );
99
  }
100
101
  /**
102
   * Returns the base filename (without the directory names).
103
   *
104
   * @return The untitled text if the path hasn't been set.
105
   */
106
  private String getTabTitle() {
107
    final Path filePath = getPath();
108
109
    return (filePath == null)
110
      ? Messages.get( "FileEditor.untitled" )
111
      : filePath.getFileName().toString();
112
  }
113
114
  /**
115
   * Returns the full filename represented by the path.
116
   *
117
   * @return The untitled text if the path hasn't been set.
118
   */
119
  private Tooltip getTabTooltip() {
120
    final Path filePath = getPath();
121
    return new Tooltip( filePath == null ? "" : filePath.toString() );
122
  }
123
124
  /**
125
   * Returns a marker to indicate whether the file has been modified.
126
   *
127
   * @return "*" when the file has changed; otherwise null.
128
   */
129
  private Text getModifiedMark() {
130
    return isModified() ? new Text( "*" ) : null;
131
  }
132
133
  /**
134
   * Called when the user switches tab.
135
   */
136
  private void activated() {
137
    // Tab is closed or no longer active.
138
    if( getTabPane() == null || !isSelected() ) {
139
      return;
140
    }
141
142
    // Switch to the tab without loading if the contents are already in memory.
143
    if( getContent() != null ) {
144
      getEditorPane().requestFocus();
145
      return;
146
    }
147
148
    // Load the text and update the preview before the undo manager.
149
    load();
150
151
    // Track undo requests -- can only be called *after* load.
152
    initUndoManager();
153
    initLayout();
154
    initFocus();
155
  }
156
157
  private void initLayout() {
158
    setContent( getScrollPane() );
159
  }
160
161
  private Node getScrollPane() {
162
    return getEditorPane().getScrollPane();
163
  }
164
165
  private void initFocus() {
166
    getEditorPane().requestFocus();
167
  }
168
169
  private void initUndoManager() {
170
    final UndoManager undoManager = getUndoManager();
171
172
    // Clear undo history after first load.
173
    undoManager.forgetHistory();
174
175
    // Bind the editor undo manager to the properties.
176
    modified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
177
    canUndo.bind( undoManager.undoAvailableProperty() );
178
    canRedo.bind( undoManager.redoAvailableProperty() );
179
  }
180
181
  /**
182
   * Searches from the caret position forward for the given string.
183
   *
184
   * @param needle The text string to match.
185
   */
186
  public void searchNext( final String needle ) {
187
    final String haystack = getEditorText();
188
    int index = haystack.indexOf( needle, getCaretPosition() );
189
190
    // Wrap around.
191
    if( index == -1 ) {
192
      index = haystack.indexOf( needle, 0 );
193
    }
194
195
    if( index >= 0 ) {
196
      setCaretPosition( index );
197
      getEditor().selectRange( index, index + needle.length() );
198
    }
199
  }
200
201
  /**
202
   * Returns the index into the text where the caret blinks happily away.
203
   *
204
   * @return A number from 0 to the editor's document text length.
205
   */
206
  public int getCaretPosition() {
207
    return getEditor().getCaretPosition();
208
  }
209
210
  /**
211
   * Moves the caret to a given offset.
212
   *
213
   * @param offset The new caret offset.
214
   */
215
  private void setCaretPosition( final int offset ) {
216
    getEditor().moveTo( offset );
217
    getEditor().requestFollowCaret();
218
  }
219
220
  /**
221
   * Returns the caret's current row and column position.
222
   *
223
   * @return The caret's offset into the document.
224
   */
225
  public Position getCaretOffset() {
226
    return getEditor().offsetToPosition( getCaretPosition(), Forward );
227
  }
228
229
  /**
230
   * Allows observers to synchronize caret position changes.
231
   *
232
   * @return An observable caret property value.
233
   */
234
  public final ObservableValue<Integer> caretPositionProperty() {
235
    return getEditor().caretPositionProperty();
236
  }
237
238
  /**
239
   * Returns the text area associated with this tab.
240
   *
241
   * @return A text editor.
242
   */
243
  private StyleClassedTextArea getEditor() {
244
    return getEditorPane().getEditor();
245
  }
246
247
  /**
248
   * Returns true if the given path exactly matches this tab's path.
249
   *
250
   * @param check The path to compare against.
251
   *
252
   * @return true The paths are the same.
253
   */
254
  public boolean isPath( final Path check ) {
255
    final Path filePath = getPath();
256
257
    return filePath == null ? false : filePath.equals( check );
258
  }
259
260
  /**
261
   * Reads the entire file contents from the path associated with this tab.
262
   */
263
  private void load() {
264
    final Path filePath = getPath();
265
266
    if( filePath != null ) {
267
      try {
268
        getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) );
269
      } catch( final Exception ex ) {
270
        getNotifyService().notify( ex );
271
      }
272
    }
273
  }
274
275
  /**
276
   * Saves the entire file contents from the path associated with this tab.
277
   *
278
   * @return true The file has been saved.
279
   */
280
  public boolean save() {
281
    try {
282
      Files.write( getPath(), asBytes( getEditorPane().getText() ) );
283
      getEditorPane().getUndoManager().mark();
284
      return true;
285
    } catch( final Exception ex ) {
286
      return alert(
287
        "FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex
288
      );
289
    }
290
  }
291
292
  /**
293
   * Creates an alert dialog and waits for it to close.
294
   *
295
   * @param titleKey Resource bundle key for the alert dialog title.
296
   * @param messageKey Resource bundle key for the alert dialog message.
297
   * @param e The unexpected happening.
298
   *
299
   * @return false
300
   */
301
  private boolean alert(
302
    final String titleKey, final String messageKey, final Exception e ) {
303
    final Notifier service = getNotifyService();
304
    final Path filePath = getPath();
305
306
    final Notification message = service.createNotification(
307
      Messages.get( titleKey ),
308
      Messages.get( messageKey ),
309
      filePath == null ? "" : filePath,
310
      e.getMessage()
311
    );
312
313
    try {
314
      service.createError( getWindow(), message ).showAndWait();
315
    } catch( final Exception ex ) {
316
      getNotifyService().notify( ex );
317
    }
318
    
319
    return false;
320
  }
321
322
  private Window getWindow() {
323
    final Scene scene = getEditorPane().getScene();
324
325
    if( scene == null ) {
326
      throw new UnsupportedOperationException( "" );
327
    }
328
329
    return scene.getWindow();
330
  }
331
332
  /**
333
   * Returns a best guess at the file encoding. If the encoding could not be
334
   * detected, this will return the default charset for the JVM.
335
   *
336
   * @param bytes The bytes to perform character encoding detection.
337
   *
338
   * @return The character encoding.
339
   */
340
  private Charset detectEncoding( final byte[] bytes ) {
341
    final UniversalDetector detector = new UniversalDetector( null );
342
    detector.handleData( bytes, 0, bytes.length );
343
    detector.dataEnd();
344
345
    final String charset = detector.getDetectedCharset();
346
    final Charset charEncoding = charset == null
347
      ? Charset.defaultCharset()
348
      : Charset.forName( charset.toUpperCase( ENGLISH ) );
349
350
    detector.reset();
351
352
    return charEncoding;
353
  }
354
355
  /**
356
   * Converts the given string to an array of bytes using the encoding that was
357
   * originally detected (if any) and associated with this file.
358
   *
359
   * @param text The text to convert into the original file encoding.
360
   *
361
   * @return A series of bytes ready for writing to a file.
362
   */
363
  private byte[] asBytes( final String text ) {
364
    return text.getBytes( getEncoding() );
365
  }
366
367
  /**
368
   * Converts the given bytes into a Java String. This will call setEncoding
369
   * with the encoding detected by the CharsetDetector.
370
   *
371
   * @param text The text of unknown character encoding.
372
   *
373
   * @return The text, in its auto-detected encoding, as a String.
374
   */
375
  private String asString( final byte[] text ) {
376
    setEncoding( detectEncoding( text ) );
377
    return new String( text, getEncoding() );
378
  }
379
380
  public Path getPath() {
381
    return this.path;
382
  }
383
384
  public void setPath( final Path path ) {
385
    this.path = path;
386
387
    updateTab();
388
  }
389
390
  /**
391
   * Answers whether this tab has an initialized path reference.
392
   *
393
   * @return false This tab has no path.
394
   */
395
  public boolean isFileOpen() {
396
    return this.path != null;
397
  }
398
399
  public boolean isModified() {
400
    return this.modified.get();
401
  }
402
403
  ReadOnlyBooleanProperty modifiedProperty() {
404
    return this.modified.getReadOnlyProperty();
405
  }
406
407
  BooleanProperty canUndoProperty() {
408
    return this.canUndo;
409
  }
410
411
  BooleanProperty canRedoProperty() {
412
    return this.canRedo;
413
  }
414
415
  private UndoManager getUndoManager() {
416
    return getEditorPane().getUndoManager();
417
  }
418
419
  /**
420
   * Forwards the request to the editor pane.
421
   *
422
   * @param <T> The type of event listener to add.
423
   * @param <U> The type of consumer to add.
424
   * @param event The event that should trigger updates to the listener.
425
   * @param consumer The listener to receive update events.
426
   */
427
  public <T extends Event, U extends T> void addEventListener(
428
    final EventPattern<? super T, ? extends U> event,
429
    final Consumer<? super U> consumer ) {
430
    getEditorPane().addEventListener( event, consumer );
431
  }
432
433
  /**
434
   * Forwards to the editor pane's listeners for keyboard events.
435
   *
436
   * @param map The new input map to replace the existing keyboard listener.
437
   */
438
  public void addEventListener( final InputMap<InputEvent> map ) {
439
    getEditorPane().addEventListener( map );
440
  }
441
442
  /**
443
   * Forwards to the editor pane's listeners for keyboard events.
444
   *
445
   * @param map The existing input map to remove from the keyboard listeners.
446
   */
447
  public void removeEventListener( final InputMap<InputEvent> map ) {
448
    getEditorPane().removeEventListener( map );
449
  }
450
451
  /**
452
   * Forwards to the editor pane's listeners for text change events.
453
   *
454
   * @param listener The listener to notify when the text changes.
455
   */
456
  public void addTextChangeListener( final ChangeListener<String> listener ) {
457
    getEditorPane().addTextChangeListener( listener );
458
  }
459
460
  /**
461
   * Forwards to the editor pane's listeners for caret paragraph change events.
462
   *
463
   * @param listener The listener to notify when the caret changes paragraphs.
464
   */
465
  public void addCaretParagraphListener( final ChangeListener<Integer> listener ) {
466
    getEditorPane().addCaretParagraphListener( listener );
467
  }
468
469
  /**
470
   * Forwards the request to the editor pane.
471
   *
472
   * @return The text to process.
473
   */
474
  public String getEditorText() {
475
    return getEditorPane().getText();
476
  }
477
478
  /**
479
   * Returns the editor pane, or creates one if it doesn't yet exist.
480
   *
481
   * @return The editor pane, never null.
482
   */
483
  public synchronized EditorPane getEditorPane() {
484
    if( this.editorPane == null ) {
485
      this.editorPane = new MarkdownEditorPane();
486
    }
487
488
    return this.editorPane;
489
  }
490
491
  private Notifier getNotifyService() {
492
    return this.alertService;
493
  }
494
495
  /**
496
   * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
497
   * determined.
498
   * 
499
   * @return The file encoding or UTF-8 if unknown.
500
   */
501
  private Charset getEncoding() {
502
    if( this.encoding == null ) {
503
      this.encoding = UTF_8;
504
    }
505
    
506
    return this.encoding;
507
  }
508
509
  private void setEncoding( final Charset encoding ) {
510
    this.encoding = encoding;
511
  }
512
513
  /**
514
   * Returns the tab title, without any modified indicators.
515
   *
516
   * @return The tab title.
517
   */
518
  @Override
519
  public String toString() {
520
    return getTabTitle();
521
  }
522
}
1523
A src/main/java/com/scrivenvar/FileEditorTabPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
31
import static com.scrivenvar.FileType.*;
32
import static com.scrivenvar.Messages.get;
33
import com.scrivenvar.predicates.files.FileTypePredicate;
34
import com.scrivenvar.service.Options;
35
import com.scrivenvar.service.Settings;
36
import com.scrivenvar.service.events.Notification;
37
import com.scrivenvar.service.events.Notifier;
38
import static com.scrivenvar.service.events.Notifier.NO;
39
import static com.scrivenvar.service.events.Notifier.YES;
40
import com.scrivenvar.util.Utils;
41
import java.io.File;
42
import java.nio.file.Path;
43
import java.util.ArrayList;
44
import java.util.List;
45
import java.util.function.Consumer;
46
import java.util.prefs.Preferences;
47
import java.util.stream.Collectors;
48
import javafx.beans.property.ReadOnlyBooleanProperty;
49
import javafx.beans.property.ReadOnlyBooleanWrapper;
50
import javafx.beans.property.ReadOnlyObjectProperty;
51
import javafx.beans.property.ReadOnlyObjectWrapper;
52
import javafx.beans.value.ChangeListener;
53
import javafx.beans.value.ObservableValue;
54
import javafx.collections.ListChangeListener;
55
import javafx.collections.ObservableList;
56
import javafx.event.Event;
57
import javafx.scene.Node;
58
import javafx.scene.control.Alert;
59
import javafx.scene.control.ButtonType;
60
import javafx.scene.control.Tab;
61
import javafx.scene.control.TabPane;
62
import javafx.scene.control.TabPane.TabClosingPolicy;
63
import javafx.scene.input.InputEvent;
64
import javafx.stage.FileChooser;
65
import javafx.stage.FileChooser.ExtensionFilter;
66
import javafx.stage.Window;
67
import org.fxmisc.richtext.StyledTextArea;
68
import org.fxmisc.wellbehaved.event.EventPattern;
69
import org.fxmisc.wellbehaved.event.InputMap;
70
71
/**
72
 * Tab pane for file editors.
73
 *
74
 * @author Karl Tauber and White Magic Software, Ltd.
75
 */
76
public final class FileEditorTabPane extends TabPane {
77
78
  private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose.filter";
79
80
  private final Options options = Services.load( Options.class );
81
  private final Settings settings = Services.load( Settings.class );
82
  private final Notifier notifyService = Services.load( Notifier.class );
83
84
  private final ReadOnlyObjectWrapper<Path> openDefinition = new ReadOnlyObjectWrapper<>();
85
  private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor = new ReadOnlyObjectWrapper<>();
86
  private final ReadOnlyBooleanWrapper anyFileEditorModified = new ReadOnlyBooleanWrapper();
87
88
  /**
89
   * Constructs a new file editor tab pane.
90
   */
91
  public FileEditorTabPane() {
92
    final ObservableList<Tab> tabs = getTabs();
93
94
    setFocusTraversable( false );
95
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
96
97
    addTabSelectionListener(
98
      (ObservableValue<? extends Tab> tabPane,
99
        final Tab oldTab, final Tab newTab) -> {
100
101
        if( newTab != null ) {
102
          activeFileEditor.set( (FileEditorTab)newTab );
103
        }
104
      }
105
    );
106
107
    final ChangeListener<Boolean> modifiedListener = (observable, oldValue, newValue) -> {
108
      for( final Tab tab : tabs ) {
109
        if( ((FileEditorTab)tab).isModified() ) {
110
          this.anyFileEditorModified.set( true );
111
          break;
112
        }
113
      }
114
    };
115
116
    tabs.addListener(
117
      (ListChangeListener<Tab>)change -> {
118
        while( change.next() ) {
119
          if( change.wasAdded() ) {
120
            change.getAddedSubList().stream().forEach( (tab) -> {
121
              ((FileEditorTab)tab).modifiedProperty().addListener( modifiedListener );
122
            } );
123
          }
124
          else if( change.wasRemoved() ) {
125
            change.getRemoved().stream().forEach( (tab) -> {
126
              ((FileEditorTab)tab).modifiedProperty().removeListener( modifiedListener );
127
            } );
128
          }
129
        }
130
131
        // Changes in the tabs may also change anyFileEditorModified property
132
        // (e.g. closed modified file)
133
        modifiedListener.changed( null, null, null );
134
      }
135
    );
136
  }
137
138
  /**
139
   * Delegates to the active file editor.
140
   *
141
   * @param <T> Event type.
142
   * @param <U> Consumer type.
143
   * @param event Event to pass to the editor.
144
   * @param consumer Consumer to pass to the editor.
145
   */
146
  public <T extends Event, U extends T> void addEventListener(
147
    final EventPattern<? super T, ? extends U> event,
148
    final Consumer<? super U> consumer ) {
149
    getActiveFileEditor().addEventListener( event, consumer );
150
  }
151
152
  /**
153
   * Delegates to the active file editor pane, and, ultimately, to its text
154
   * area.
155
   *
156
   * @param map The map of methods to events.
157
   */
158
  public void addEventListener( final InputMap<InputEvent> map ) {
159
    getActiveFileEditor().addEventListener( map );
160
  }
161
162
  /**
163
   * Remove a keyboard event listener from the active file editor.
164
   *
165
   * @param map The keyboard events to remove.
166
   */
167
  public void removeEventListener( final InputMap<InputEvent> map ) {
168
    getActiveFileEditor().removeEventListener( map );
169
  }
170
171
  /**
172
   * Allows observers to be notified when the current file editor tab changes.
173
   *
174
   * @param listener The listener to notify of tab change events.
175
   */
176
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
177
    // Observe the tab so that when a new tab is opened or selected,
178
    // a notification is kicked off.
179
    getSelectionModel().selectedItemProperty().addListener( listener );
180
  }
181
182
  /**
183
   * Allows clients to manipulate the editor content directly.
184
   *
185
   * @return The text area for the active file editor.
186
   */
187
  public StyledTextArea getEditor() {
188
    return getActiveFileEditor().getEditorPane().getEditor();
189
  }
190
191
  public FileEditorTab getActiveFileEditor() {
192
    return this.activeFileEditor.get();
193
  }
194
195
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
196
    return this.activeFileEditor.getReadOnlyProperty();
197
  }
198
199
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
200
    return this.anyFileEditorModified.getReadOnlyProperty();
201
  }
202
203
  private FileEditorTab createFileEditor( final Path path ) {
204
    final FileEditorTab tab = new FileEditorTab( path );
205
206
    tab.setOnCloseRequest( e -> {
207
      if( !canCloseEditor( tab ) ) {
208
        e.consume();
209
      }
210
    } );
211
212
    return tab;
213
  }
214
215
  /**
216
   * Called when the user selects New from the File menu.
217
   *
218
   * @return The newly added tab.
219
   */
220
  void newEditor() {
221
    final FileEditorTab tab = createFileEditor( null );
222
223
    getTabs().add( tab );
224
    getSelectionModel().select( tab );
225
  }
226
227
  void openFileDialog() {
228
    final String title = get( "Dialog.file.choose.open.title" );
229
    final FileChooser dialog = createFileChooser( title );
230
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
231
232
    if( files != null ) {
233
      openFiles( files );
234
    }
235
  }
236
237
  /**
238
   * Opens the files into new editors, unless one of those files was a
239
   * definition file. The definition file is loaded into the definition pane,
240
   * but only the first one selected (multiple definition files will result in a
241
   * warning).
242
   *
243
   * @param files The list of non-definition files that the were requested to
244
   * open.
245
   *
246
   * @return A list of files that can be opened in text editors.
247
   */
248
  private void openFiles( final List<File> files ) {
249
    final FileTypePredicate predicate
250
      = new FileTypePredicate( createExtensionFilter( DEFINITION ).getExtensions() );
251
252
    // The user might have opened multiple definitions files. These will
253
    // be discarded from the text editable files.
254
    final List<File> definitions
255
      = files.stream().filter( predicate ).collect( Collectors.toList() );
256
257
    // Create a modifiable list to remove any definition files that were
258
    // opened.
259
    final List<File> editors = new ArrayList<>( files );
260
261
    if( editors.size() > 0 ) {
262
      saveLastDirectory( editors.get( 0 ) );
263
    }
264
265
    editors.removeAll( definitions );
266
267
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
268
    if( editors.size() > 0 ) {
269
      openEditors( editors, 0 );
270
    }
271
272
    if( definitions.size() > 0 ) {
273
      openDefinition( definitions.get( 0 ) );
274
    }
275
  }
276
277
  private void openEditors( final List<File> files, final int activeIndex ) {
278
    final int fileTally = files.size();
279
    final List<Tab> tabs = getTabs();
280
281
    // Close single unmodified "Untitled" tab.
282
    if( tabs.size() == 1 ) {
283
      final FileEditorTab fileEditor = (FileEditorTab)(tabs.get( 0 ));
284
285
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
286
        closeEditor( fileEditor, false );
287
      }
288
    }
289
290
    for( int i = 0; i < fileTally; i++ ) {
291
      final Path path = files.get( i ).toPath();
292
293
      FileEditorTab fileEditorTab = findEditor( path );
294
295
      // Only open new files.
296
      if( fileEditorTab == null ) {
297
        fileEditorTab = createFileEditor( path );
298
        getTabs().add( fileEditorTab );
299
      }
300
301
      // Select the first file in the list.
302
      if( i == activeIndex ) {
303
        getSelectionModel().select( fileEditorTab );
304
      }
305
    }
306
  }
307
308
  /**
309
   * Returns a property that changes when a new definition file is opened.
310
   *
311
   * @return The path to a definition file that was opened.
312
   */
313
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
314
    return getOnOpenDefinitionFile().getReadOnlyProperty();
315
  }
316
317
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
318
    return this.openDefinition;
319
  }
320
321
  /**
322
   * Called when the user has opened a definition file (using the file open
323
   * dialog box). This will replace the current set of definitions for the
324
   * active tab.
325
   *
326
   * @param definition The file to open.
327
   */
328
  private void openDefinition( final File definition ) {
329
    // TODO: Prevent reading this file twice when a new text document is opened.
330
    // (might be a matter of checking the value first).
331
    getOnOpenDefinitionFile().set( definition.toPath() );
332
  }
333
334
  boolean saveEditor( final FileEditorTab tab ) {
335
    if( tab == null || !tab.isModified() ) {
336
      return true;
337
    }
338
339
    return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
340
  }
341
342
  boolean saveEditorAs( final FileEditorTab tab ) {
343
    if( tab == null ) {
344
      return true;
345
    }
346
347
    getSelectionModel().select( tab );
348
349
    final FileChooser fileChooser = createFileChooser( get( "Dialog.file.choose.save.title" ) );
350
    final File file = fileChooser.showSaveDialog( getWindow() );
351
    if( file == null ) {
352
      return false;
353
    }
354
355
    saveLastDirectory( file );
356
    tab.setPath( file.toPath() );
357
358
    return tab.save();
359
  }
360
361
  boolean saveAllEditors() {
362
    boolean success = true;
363
364
    for( FileEditorTab fileEditor : getAllEditors() ) {
365
      if( !saveEditor( fileEditor ) ) {
366
        success = false;
367
      }
368
    }
369
370
    return success;
371
  }
372
373
  /**
374
   * Answers whether the file has had modifications. '
375
   *
376
   * @param tab THe tab to check for modifications.
377
   *
378
   * @return false The file is unmodified.
379
   */
380
  boolean canCloseEditor( final FileEditorTab tab ) {
381
    if( !tab.isModified() ) {
382
      return true;
383
    }
384
385
    final Notification message = getNotifyService().createNotification(
386
      Messages.get( "Alert.file.close.title" ),
387
      Messages.get( "Alert.file.close.text" ),
388
      tab.getText()
389
    );
390
391
    final Alert alert = getNotifyService().createConfirmation(
392
      getWindow(), message );
393
    final ButtonType response = alert.showAndWait().get();
394
395
    return response == YES ? saveEditor( tab ) : response == NO;
396
  }
397
398
  private Notifier getNotifyService() {
399
    return this.notifyService;
400
  }
401
402
  boolean closeEditor( FileEditorTab fileEditor, boolean save ) {
403
    if( fileEditor == null ) {
404
      return true;
405
    }
406
407
    final Tab tab = fileEditor;
408
409
    if( save ) {
410
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
411
      Event.fireEvent( tab, event );
412
413
      if( event.isConsumed() ) {
414
        return false;
415
      }
416
    }
417
418
    getTabs().remove( tab );
419
420
    if( tab.getOnClosed() != null ) {
421
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
422
    }
423
424
    return true;
425
  }
426
427
  boolean closeAllEditors() {
428
    final FileEditorTab[] allEditors = getAllEditors();
429
    final FileEditorTab activeEditor = getActiveFileEditor();
430
431
    // try to save active tab first because in case the user decides to cancel,
432
    // then it stays active
433
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
434
      return false;
435
    }
436
437
    // This should be called any time a tab changes.
438
    persistPreferences();
439
440
    // save modified tabs
441
    for( int i = 0; i < allEditors.length; i++ ) {
442
      final FileEditorTab fileEditor = allEditors[ i ];
443
444
      if( fileEditor == activeEditor ) {
445
        continue;
446
      }
447
448
      if( fileEditor.isModified() ) {
449
        // activate the modified tab to make its modified content visible to the user
450
        getSelectionModel().select( i );
451
452
        if( !canCloseEditor( fileEditor ) ) {
453
          return false;
454
        }
455
      }
456
    }
457
458
    // Close all tabs.
459
    for( final FileEditorTab fileEditor : allEditors ) {
460
      if( !closeEditor( fileEditor, false ) ) {
461
        return false;
462
      }
463
    }
464
465
    return getTabs().isEmpty();
466
  }
467
468
  private FileEditorTab[] getAllEditors() {
469
    final ObservableList<Tab> tabs = getTabs();
470
    final int length = tabs.size();
471
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
472
473
    for( int i = 0; i < length; i++ ) {
474
      allEditors[ i ] = (FileEditorTab)tabs.get( i );
475
    }
476
477
    return allEditors;
478
  }
479
480
  /**
481
   * Returns the file editor tab that has the given path.
482
   *
483
   * @return null No file editor tab for the given path was found.
484
   */
485
  private FileEditorTab findEditor( final Path path ) {
486
    for( final Tab tab : getTabs() ) {
487
      final FileEditorTab fileEditor = (FileEditorTab)tab;
488
489
      if( fileEditor.isPath( path ) ) {
490
        return fileEditor;
491
      }
492
    }
493
494
    return null;
495
  }
496
497
  private FileChooser createFileChooser( String title ) {
498
    final FileChooser fileChooser = new FileChooser();
499
500
    fileChooser.setTitle( title );
501
    fileChooser.getExtensionFilters().addAll(
502
      createExtensionFilters() );
503
504
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
505
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
506
507
    if( !file.isDirectory() ) {
508
      file = new File( "." );
509
    }
510
511
    fileChooser.setInitialDirectory( file );
512
    return fileChooser;
513
  }
514
515
  private List<ExtensionFilter> createExtensionFilters() {
516
    final List<ExtensionFilter> list = new ArrayList<>();
517
518
    // TODO: Return a list of all properties that match the filter prefix.
519
    // This will allow dynamic filters to be added and removed just by
520
    // updating the properties file.
521
    list.add( createExtensionFilter( MARKDOWN ) );
522
    list.add( createExtensionFilter( DEFINITION ) );
523
    list.add( createExtensionFilter( XML ) );
524
    list.add( createExtensionFilter( ALL ) );
525
    return list;
526
  }
527
528
  /**
529
   * Returns a filter for file name extensions recognized by the application
530
   * that can be opened by the user.
531
   *
532
   * @param filetype Used to find the globbing pattern for extensions.
533
   *
534
   * @return A filename filter suitable for use by a FileDialog instance.
535
   */
536
  private ExtensionFilter createExtensionFilter( final FileType filetype ) {
537
    final String tKey = String.format( "%s.title.%s", FILTER_EXTENSION_TITLES, filetype );
538
    final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
539
540
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
541
  }
542
543
  private List<String> getExtensions( final String key ) {
544
    return getSettings().getStringSettingList( key );
545
  }
546
547
  private void saveLastDirectory( final File file ) {
548
    getPreferences().put( "lastDirectory", file.getParent() );
549
  }
550
551
  public void restorePreferences() {
552
    int activeIndex = 0;
553
554
    final Preferences preferences = getPreferences();
555
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
556
    final String activeFileName = preferences.get( "activeFile", null );
557
558
    final ArrayList<File> files = new ArrayList<>( fileNames.length );
559
560
    for( final String fileName : fileNames ) {
561
      final File file = new File( fileName );
562
563
      if( file.exists() ) {
564
        files.add( file );
565
566
        if( fileName.equals( activeFileName ) ) {
567
          activeIndex = files.size() - 1;
568
        }
569
      }
570
    }
571
572
    if( files.isEmpty() ) {
573
      newEditor();
574
    }
575
    else {
576
      openEditors( files, activeIndex );
577
    }
578
  }
579
580
  public void persistPreferences() {
581
    final ObservableList<Tab> allEditors = getTabs();
582
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
583
584
    for( final Tab tab : allEditors ) {
585
      final FileEditorTab fileEditor = (FileEditorTab)tab;
586
      final Path filePath = fileEditor.getPath();
587
588
      if( filePath != null ) {
589
        fileNames.add( filePath.toString() );
590
      }
591
    }
592
593
    final Preferences preferences = getPreferences();
594
    Utils.putPrefsStrings( preferences, "file", fileNames.toArray( new String[ fileNames.size() ] ) );
595
596
    final FileEditorTab activeEditor = getActiveFileEditor();
597
    final Path filePath = activeEditor == null ? null : activeEditor.getPath();
598
599
    if( filePath == null ) {
600
      preferences.remove( "activeFile" );
601
    }
602
    else {
603
      preferences.put( "activeFile", filePath.toString() );
604
    }
605
  }
606
607
  private Settings getSettings() {
608
    return this.settings;
609
  }
610
611
  protected Options getOptions() {
612
    return this.options;
613
  }
614
615
  private Window getWindow() {
616
    return getScene().getWindow();
617
  }
618
619
  private Preferences getPreferences() {
620
    return getOptions().getState();
621
  }
622
623
  Node getNode() {
624
    return this;
625
  }
626
}
1627
A src/main/java/com/scrivenvar/FileType.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
/**
31
 * Represents different file type classifications. These are high-level mappings
32
 * that correspond to the list of glob patterns found within
33
 * settings.properties.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public enum FileType {
38
39
  ALL( "all" ),
40
  RMARKDOWN( "rmarkdown" ),
41
  RXML( "rxml" ),
42
  MARKDOWN( "markdown" ),
43
  DEFINITION( "definition" ),
44
  XML( "xml" ),
45
  CSV( "csv" ),
46
  JSON( "json" ),
47
  TOML( "toml" ),
48
  YAML( "yaml" ),
49
  PROPERTIES( "properties" );
50
51
  private final String type;
52
53
  private FileType( final String type ) {
54
    this.type = type;
55
  }
56
57
  /**
58
   * Returns the file type that corresponds to the given string.
59
   *
60
   * @param type The string to compare against this enumeration of file types.
61
   *
62
   * @return The corresponding File Type for the given string.
63
   *
64
   * @throws IllegalArgumentException Type not found.
65
   */
66
  public static FileType from( final String type ) {
67
    for( final FileType fileType : FileType.values() ) {
68
      if( fileType.isType( type ) ) {
69
        return fileType;
70
      }
71
    }
72
73
    throw new IllegalArgumentException( type );
74
  }
75
76
  /**
77
   * Answers whether this file type matches the given string, case insensitive
78
   * comparison.
79
   *
80
   * @param type Presumably a file name extension to check against.
81
   *
82
   * @return true The given extension corresponds to this enumerated type.
83
   */
84
  public boolean isType( final String type ) {
85
    return getType().equalsIgnoreCase( type );
86
  }
87
88
  private String getType() {
89
    return this.type;
90
  }
91
92
  /**
93
   * Returns the lowercase version of the file name extension.
94
   *
95
   * @return The file name, in lower case.
96
   */
97
  @Override
98
  public String toString() {
99
    return getType();
100
  }
101
}
1102
A src/main/java/com/scrivenvar/Main.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import static com.scrivenvar.Constants.*;
31
import static com.scrivenvar.Messages.get;
32
import com.scrivenvar.preferences.FilePreferencesFactory;
33
import com.scrivenvar.service.Options;
34
import com.scrivenvar.service.Snitch;
35
import com.scrivenvar.service.events.Notifier;
36
import com.scrivenvar.util.StageState;
37
import java.util.logging.LogManager;
38
import javafx.application.Application;
39
import javafx.scene.Scene;
40
import javafx.scene.image.Image;
41
import javafx.stage.Stage;
42
43
/**
44
 * Main application entry point. The application allows users to edit Markdown
45
 * files and see a real-time preview of the edits.
46
 *
47
 * @author Karl Tauber and White Magic Software, Ltd.
48
 */
49
public final class Main extends Application {
50
51
  private Options options;
52
  private Snitch snitch;
53
  private Thread snitchThread;
54
55
  private static Application app;
56
  private final MainWindow mainWindow = new MainWindow();
57
58
  public static void main( final String[] args ) {
59
    initLogger();
60
    initPreferences();
61
    launch( args );
62
  }
63
64
  /**
65
   * Prevents JavaFX from logging to standard error.
66
   *
67
   * @see http://stackoverflow.com/a/41476462/59087
68
   */
69
  private static void initLogger() {
70
    LogManager.getLogManager().reset();
71
  }
72
73
  /**
74
   * Sets the factory used for reading user preferences.
75
   */
76
  private static void initPreferences() {
77
    System.setProperty(
78
      "java.util.prefs.PreferencesFactory",
79
      FilePreferencesFactory.class.getName()
80
    );
81
  }
82
83
  /**
84
   * Application entry point.
85
   *
86
   * @param stage The primary application stage.
87
   *
88
   * @throws Exception Could not read configuration file.
89
   */
90
  @Override
91
  public void start( final Stage stage ) throws Exception {
92
    initApplication();
93
    initNotifyService();
94
    initState( stage );
95
    initStage( stage );
96
    initSnitch();
97
98
    stage.show();
99
  }
100
101
  public static void showDocument( final String uri ) {
102
    getApplication().getHostServices().showDocument( uri );
103
  }
104
105
  private void initApplication() {
106
    app = this;
107
  }
108
109
  /**
110
   * Constructs the notify service and appends the main window to the list of
111
   * notification observers.
112
   */
113
  private void initNotifyService() {
114
    final Notifier notifier = Services.load( Notifier.class );
115
    notifier.addObserver( getMainWindow() );
116
  }
117
118
  private StageState initState( final Stage stage ) {
119
    return new StageState( stage, getOptions().getState() );
120
  }
121
122
  private void initStage( final Stage stage ) {
123
    stage.getIcons().addAll(
124
      createImage( FILE_LOGO_16 ),
125
      createImage( FILE_LOGO_32 ),
126
      createImage( FILE_LOGO_128 ),
127
      createImage( FILE_LOGO_256 ),
128
      createImage( FILE_LOGO_512 ) );
129
    stage.setTitle( getApplicationTitle() );
130
    stage.setScene( getScene() );
131
  }
132
133
  private void initSnitch() {
134
    setSnitchThread( new Thread( getSnitch() ) );
135
    getSnitchThread().start();
136
  }
137
138
  /**
139
   * Stops the snitch service, if its running.
140
   *
141
   * @throws InterruptedException Couldn't stop the snitch thread.
142
   */
143
  @Override
144
  public void stop() throws InterruptedException {
145
    getSnitch().stop();
146
147
    final Thread thread = getSnitchThread();
148
149
    if( thread != null ) {
150
      thread.interrupt();
151
      thread.join();
152
    }
153
  }
154
155
  private synchronized Snitch getSnitch() {
156
    if( this.snitch == null ) {
157
      this.snitch = Services.load( Snitch.class );
158
    }
159
160
    return this.snitch;
161
  }
162
163
  private Thread getSnitchThread() {
164
    return this.snitchThread;
165
  }
166
167
  private void setSnitchThread( final Thread thread ) {
168
    this.snitchThread = thread;
169
  }
170
171
  private synchronized Options getOptions() {
172
    if( this.options == null ) {
173
      this.options = Services.load( Options.class );
174
    }
175
176
    return this.options;
177
  }
178
179
  private Scene getScene() {
180
    return getMainWindow().getScene();
181
  }
182
183
  private MainWindow getMainWindow() {
184
    return this.mainWindow;
185
  }
186
187
  private String getApplicationTitle() {
188
    return get( "Main.title" );
189
  }
190
191
  private static Application getApplication() {
192
    return app;
193
  }
194
195
  private Image createImage( final String filename ) {
196
    return new Image( filename );
197
  }
198
}
1199
A src/main/java/com/scrivenvar/MainWindow.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import static com.scrivenvar.Constants.*;
31
import static com.scrivenvar.Messages.get;
32
import com.scrivenvar.definition.*;
33
import com.scrivenvar.dialogs.RScriptDialog;
34
import com.scrivenvar.editors.EditorPane;
35
import com.scrivenvar.editors.VariableNameInjector;
36
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
37
import com.scrivenvar.predicates.files.FileTypePredicate;
38
import com.scrivenvar.preview.HTMLPreviewPane;
39
import com.scrivenvar.processors.Processor;
40
import com.scrivenvar.processors.ProcessorFactory;
41
import com.scrivenvar.service.Options;
42
import com.scrivenvar.service.Snitch;
43
import com.scrivenvar.service.events.Notifier;
44
import com.scrivenvar.util.Action;
45
import com.scrivenvar.util.ActionUtils;
46
import static com.scrivenvar.util.StageState.*;
47
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
48
import java.io.IOException;
49
import java.nio.file.Path;
50
import java.util.HashMap;
51
import java.util.Map;
52
import java.util.Observable;
53
import java.util.Observer;
54
import java.util.Optional;
55
import java.util.function.Function;
56
import java.util.prefs.Preferences;
57
import javafx.application.Platform;
58
import javafx.beans.binding.Bindings;
59
import javafx.beans.binding.BooleanBinding;
60
import javafx.beans.property.BooleanProperty;
61
import javafx.beans.property.SimpleBooleanProperty;
62
import javafx.beans.value.ObservableBooleanValue;
63
import javafx.beans.value.ObservableValue;
64
import javafx.collections.ListChangeListener.Change;
65
import javafx.collections.ObservableList;
66
import static javafx.event.Event.fireEvent;
67
import javafx.geometry.Pos;
68
import javafx.scene.Node;
69
import javafx.scene.Scene;
70
import javafx.scene.control.Alert;
71
import javafx.scene.control.Alert.AlertType;
72
import javafx.scene.control.Menu;
73
import javafx.scene.control.MenuBar;
74
import javafx.scene.control.SplitPane;
75
import javafx.scene.control.Tab;
76
import javafx.scene.control.TextField;
77
import javafx.scene.control.ToolBar;
78
import javafx.scene.control.TreeView;
79
import javafx.scene.image.Image;
80
import javafx.scene.image.ImageView;
81
import static javafx.scene.input.KeyCode.ESCAPE;
82
import javafx.scene.input.KeyEvent;
83
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
84
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
85
import javafx.scene.layout.BorderPane;
86
import javafx.scene.layout.VBox;
87
import javafx.scene.text.Text;
88
import javafx.stage.Window;
89
import javafx.stage.WindowEvent;
90
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
91
import org.controlsfx.control.StatusBar;
92
import org.fxmisc.richtext.model.TwoDimensional.Position;
93
94
/**
95
 * Main window containing a tab pane in the center for file editors.
96
 *
97
 * @author Karl Tauber and White Magic Software, Ltd.
98
 */
99
public class MainWindow implements Observer {
100
101
  private final Options options = Services.load( Options.class );
102
  private final Snitch snitch = Services.load( Snitch.class );
103
  private final Notifier notifier = Services.load( Notifier.class );
104
105
  private Scene scene;
106
  private MenuBar menuBar;
107
  private StatusBar statusBar;
108
  private Text lineNumberText;
109
  private TextField findTextField;
110
111
  private DefinitionSource definitionSource;
112
  private DefinitionPane definitionPane;
113
  private FileEditorTabPane fileEditorPane;
114
  private HTMLPreviewPane previewPane;
115
116
  /**
117
   * Prevents re-instantiation of processing classes.
118
   */
119
  private Map<FileEditorTab, Processor<String>> processors;
120
121
  public MainWindow() {
122
    initLayout();
123
    initFindInput();
124
    initSnitch();
125
    initDefinitionListener();
126
    initTabAddedListener();
127
    initTabChangedListener();
128
    initPreferences();
129
  }
130
131
  /**
132
   * Watch for changes to external files. In particular, this awaits
133
   * modifications to any XSL files associated with XML files being edited. When
134
   * an XSL file is modified (external to the application), the snitch's ears
135
   * perk up and the file is reloaded. This keeps the XSL transformation up to
136
   * date with what's on the file system.
137
   */
138
  private void initSnitch() {
139
    getSnitch().addObserver( this );
140
  }
141
142
  /**
143
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
144
   * presses.
145
   */
146
  private void initFindInput() {
147
    final TextField input = getFindTextField();
148
149
    input.setOnKeyPressed( (KeyEvent event) -> {
150
      switch( event.getCode() ) {
151
        case F3:
152
        case ENTER:
153
          findNext();
154
          break;
155
        case F:
156
          if( !event.isControlDown() ) {
157
            break;
158
          }
159
        case ESCAPE:
160
          getStatusBar().setGraphic( null );
161
          getActiveFileEditor().getEditorPane().requestFocus();
162
          break;
163
      }
164
    } );
165
166
    // Remove when the input field loses focus.
167
    input.focusedProperty().addListener(
168
      (
169
        final ObservableValue<? extends Boolean> focused,
170
        final Boolean oFocus,
171
        final Boolean nFocus) -> {
172
        if( !nFocus ) {
173
          getStatusBar().setGraphic( null );
174
        }
175
      }
176
    );
177
  }
178
179
  /**
180
   * Listen for file editor tab pane to receive an open definition source event.
181
   */
182
  private void initDefinitionListener() {
183
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
184
      (ObservableValue<? extends Path> definitionFile,
185
        final Path oldPath, final Path newPath) -> {
186
        openDefinition( newPath );
187
188
        // Indirectly refresh the resolved map.
189
        setProcessors( null );
190
        updateDefinitionPane();
191
192
        try {
193
          getSnitch().ignore( oldPath );
194
          getSnitch().listen( newPath );
195
        } catch( final IOException ex ) {
196
          error( ex );
197
        }
198
199
        // Will create new processors and therefore a new resolved map.
200
        refreshSelectedTab( getActiveFileEditor() );
201
      }
202
    );
203
  }
204
205
  /**
206
   * When tabs are added, hook the various change listeners onto the new tab so
207
   * that the preview pane refreshes as necessary.
208
   */
209
  private void initTabAddedListener() {
210
    final FileEditorTabPane editorPane = getFileEditorPane();
211
212
    // Make sure the text processor kicks off when new files are opened.
213
    final ObservableList<Tab> tabs = editorPane.getTabs();
214
215
    // Update the preview pane on tab changes.
216
    tabs.addListener(
217
      (final Change<? extends Tab> change) -> {
218
        while( change.next() ) {
219
          if( change.wasAdded() ) {
220
            // Multiple tabs can be added simultaneously.
221
            for( final Tab newTab : change.getAddedSubList() ) {
222
              final FileEditorTab tab = (FileEditorTab)newTab;
223
224
              initTextChangeListener( tab );
225
              initCaretParagraphListener( tab );
226
              initVariableNameInjector( tab );
227
//              initSyntaxListener( tab );
228
            }
229
          }
230
        }
231
      }
232
    );
233
  }
234
235
  /**
236
   * Reloads the preferences from the previous session.
237
   */
238
  private void initPreferences() {
239
    restoreDefinitionSource();
240
    getFileEditorPane().restorePreferences();
241
    updateDefinitionPane();
242
  }
243
244
  /**
245
   * Listen for new tab selection events.
246
   */
247
  private void initTabChangedListener() {
248
    final FileEditorTabPane editorPane = getFileEditorPane();
249
250
    // Update the preview pane changing tabs.
251
    editorPane.addTabSelectionListener(
252
      (ObservableValue<? extends Tab> tabPane,
253
        final Tab oldTab, final Tab newTab) -> {
254
255
        // If there was no old tab, then this is a first time load, which
256
        // can be ignored.
257
        if( oldTab != null ) {
258
          if( newTab == null ) {
259
            closeRemainingTab();
260
          }
261
          else {
262
            // Update the preview with the edited text.
263
            refreshSelectedTab( (FileEditorTab)newTab );
264
          }
265
        }
266
      }
267
    );
268
  }
269
270
  private void initTextChangeListener( final FileEditorTab tab ) {
271
    tab.addTextChangeListener(
272
      (ObservableValue<? extends String> editor,
273
        final String oldValue, final String newValue) -> {
274
        refreshSelectedTab( tab );
275
      }
276
    );
277
  }
278
279
  private void initCaretParagraphListener( final FileEditorTab tab ) {
280
    tab.addCaretParagraphListener(
281
      (ObservableValue<? extends Integer> editor,
282
        final Integer oldValue, final Integer newValue) -> {
283
        refreshSelectedTab( tab );
284
      }
285
    );
286
  }
287
288
  private void initVariableNameInjector( final FileEditorTab tab ) {
289
    VariableNameInjector.listen( tab, getDefinitionPane() );
290
  }
291
292
  /**
293
   * Called whenever the preview pane becomes out of sync with the file editor
294
   * tab. This can be called when the text changes, the caret paragraph changes,
295
   * or the file tab changes.
296
   *
297
   * @param tab The file editor tab that has been changed in some fashion.
298
   */
299
  private void refreshSelectedTab( final FileEditorTab tab ) {
300
    if( tab.isFileOpen() ) {
301
      getPreviewPane().setPath( tab.getPath() );
302
303
      // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
304
      final Position p = tab.getCaretOffset();
305
      getLineNumberText().setText(
306
        get( STATUS_BAR_LINE,
307
          p.getMajor() + 1,
308
          p.getMinor() + 1,
309
          tab.getCaretPosition() + 1
310
        )
311
      );
312
313
      Processor<String> processor = getProcessors().get( tab );
314
315
      if( processor == null ) {
316
        processor = createProcessor( tab );
317
        getProcessors().put( tab, processor );
318
      }
319
320
      try {
321
        getNotifier().clear();
322
        processor.processChain( tab.getEditorText() );
323
      } catch( final Exception ex ) {
324
        error( ex );
325
      }
326
    }
327
  }
328
329
  /**
330
   * Used to find text in the active file editor window.
331
   */
332
  private void find() {
333
    final TextField input = getFindTextField();
334
    getStatusBar().setGraphic( input );
335
    input.requestFocus();
336
  }
337
338
  public void findNext() {
339
    getActiveFileEditor().searchNext( getFindTextField().getText() );
340
  }
341
342
  /**
343
   * Returns the variable map of interpolated definitions.
344
   *
345
   * @return A map to help dereference variables.
346
   */
347
  private Map<String, String> getResolvedMap() {
348
    return getDefinitionSource().getResolvedMap();
349
  }
350
351
  /**
352
   * Returns the root node for the hierarchical definition source.
353
   *
354
   * @return Data to display in the definition pane.
355
   */
356
  private TreeView<String> getTreeView() {
357
    try {
358
      return getDefinitionSource().asTreeView();
359
    } catch( Exception e ) {
360
      error( e );
361
    }
362
363
    // Slightly redundant as getDefinitionSource() might have returned an
364
    // empty definition source.
365
    return (new EmptyDefinitionSource()).asTreeView();
366
  }
367
368
  /**
369
   * Called when a definition source is opened.
370
   *
371
   * @param path Path to the definition source that was opened.
372
   */
373
  private void openDefinition( final Path path ) {
374
    try {
375
      final DefinitionSource ds = createDefinitionSource( path.toString() );
376
      setDefinitionSource( ds );
377
      storeDefinitionSource();
378
      updateDefinitionPane();
379
    } catch( final Exception e ) {
380
      error( e );
381
    }
382
  }
383
384
  private void updateDefinitionPane() {
385
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
386
  }
387
388
  private void restoreDefinitionSource() {
389
    final Preferences preferences = getPreferences();
390
    final String source = preferences.get( PERSIST_DEFINITION_SOURCE, null );
391
392
    // If there's no definition source set, don't try to load it.
393
    if( source != null ) {
394
      setDefinitionSource( createDefinitionSource( source ) );
395
    }
396
  }
397
398
  private void storeDefinitionSource() {
399
    final Preferences preferences = getPreferences();
400
    final DefinitionSource ds = getDefinitionSource();
401
402
    preferences.put( PERSIST_DEFINITION_SOURCE, ds.toString() );
403
  }
404
405
  /**
406
   * Called when the last open tab is closed to clear the preview pane.
407
   */
408
  private void closeRemainingTab() {
409
    getPreviewPane().clear();
410
  }
411
412
  /**
413
   * Called when an exception occurs that warrants the user's attention.
414
   *
415
   * @param e The exception with a message that the user should know about.
416
   */
417
  private void error( final Exception e ) {
418
    getNotifier().notify( e );
419
  }
420
421
  //---- File actions -------------------------------------------------------
422
  /**
423
   * Called when an observable instance has changed. This is called by both the
424
   * snitch service and the notify service. The snitch service can be called for
425
   * different file types, including definition sources.
426
   *
427
   * @param observable The observed instance.
428
   * @param value The noteworthy item.
429
   */
430
  @Override
431
  public void update( final Observable observable, final Object value ) {
432
    if( value != null ) {
433
      if( observable instanceof Snitch && value instanceof Path ) {
434
        final Path path = (Path)value;
435
        final FileTypePredicate predicate
436
          = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
437
438
        // Reload definitions.
439
        if( predicate.test( path.toFile() ) ) {
440
          updateDefinitionSource( path );
441
        }
442
443
        updateSelectedTab();
444
      }
445
      else if( observable instanceof Notifier && value instanceof String ) {
446
        updateStatusBar( (String)value );
447
      }
448
    }
449
  }
450
451
  /**
452
   * Updates the status bar to show the given message.
453
   *
454
   * @param s The message to show in the status bar.
455
   */
456
  private void updateStatusBar( final String s ) {
457
    Platform.runLater(
458
      () -> {
459
        final int index = s.indexOf( '\n' );
460
        final String message = s.substring( 0, index > 0 ? index : s.length() );
461
462
        getStatusBar().setText( message );
463
      }
464
    );
465
  }
466
467
  /**
468
   * Called when a file has been modified.
469
   *
470
   * @param file Path to the modified file.
471
   */
472
  private void updateSelectedTab() {
473
    Platform.runLater(
474
      () -> {
475
        // Brute-force XSLT file reload by re-instantiating all processors.
476
        resetProcessors();
477
        refreshSelectedTab( getActiveFileEditor() );
478
      }
479
    );
480
  }
481
482
  /**
483
   * Reloads the definition source from the given path.
484
   *
485
   * @param path The path containing new definition information.
486
   */
487
  private void updateDefinitionSource( final Path path ) {
488
    Platform.runLater(
489
      () -> {
490
        openDefinition( path );
491
      }
492
    );
493
  }
494
495
  /**
496
   * After resetting the processors, they will refresh anew to be up-to-date
497
   * with the files (text and definition) currently loaded into the editor.
498
   */
499
  private void resetProcessors() {
500
    getProcessors().clear();
501
  }
502
503
  //---- File actions -------------------------------------------------------
504
  private void fileNew() {
505
    getFileEditorPane().newEditor();
506
  }
507
508
  private void fileOpen() {
509
    getFileEditorPane().openFileDialog();
510
  }
511
512
  private void fileClose() {
513
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
514
  }
515
516
  private void fileCloseAll() {
517
    getFileEditorPane().closeAllEditors();
518
  }
519
520
  private void fileSave() {
521
    getFileEditorPane().saveEditor( getActiveFileEditor() );
522
  }
523
524
  private void fileSaveAs() {
525
    final FileEditorTab editor = getActiveFileEditor();
526
    getFileEditorPane().saveEditorAs( editor );
527
    getProcessors().remove( editor );
528
529
    try {
530
      refreshSelectedTab( editor );
531
    } catch( final Exception ex ) {
532
      getNotifier().notify( ex );
533
    }
534
  }
535
536
  private void fileSaveAll() {
537
    getFileEditorPane().saveAllEditors();
538
  }
539
540
  private void fileExit() {
541
    final Window window = getWindow();
542
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
543
  }
544
545
  //---- Tools actions
546
  private void toolsScript() {
547
    final String script = getStartupScript();
548
549
    final RScriptDialog dialog = new RScriptDialog(
550
      getWindow(), "Dialog.rScript.title", script );
551
    final Optional<String> result = dialog.showAndWait();
552
553
    result.ifPresent( (String s) -> {
554
      putStartupScript( s );
555
    } );
556
  }
557
558
  /**
559
   * Gets the R startup script from the user preferences.
560
   */
561
  private String getStartupScript() {
562
    return getPreferences().get( PERSIST_R_STARTUP, "" );
563
  }
564
565
  /**
566
   * Puts an R startup script into the user preferences.
567
   */
568
  private void putStartupScript( final String s ) {
569
    try {
570
      getPreferences().put( PERSIST_R_STARTUP, s );
571
    } catch( final Exception ex ) {
572
      getNotifier().notify( ex );
573
    }
574
  }
575
576
  //---- Help actions -------------------------------------------------------
577
  private void helpAbout() {
578
    Alert alert = new Alert( AlertType.INFORMATION );
579
    alert.setTitle( get( "Dialog.about.title" ) );
580
    alert.setHeaderText( get( "Dialog.about.header" ) );
581
    alert.setContentText( get( "Dialog.about.content" ) );
582
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
583
    alert.initOwner( getWindow() );
584
585
    alert.showAndWait();
586
  }
587
588
  //---- Convenience accessors ----------------------------------------------
589
  private float getFloat( final String key, final float defaultValue ) {
590
    return getPreferences().getFloat( key, defaultValue );
591
  }
592
593
  private Preferences getPreferences() {
594
    return getOptions().getState();
595
  }
596
597
  protected Scene getScene() {
598
    if( this.scene == null ) {
599
      this.scene = createScene();
600
    }
601
602
    return this.scene;
603
  }
604
605
  public Window getWindow() {
606
    return getScene().getWindow();
607
  }
608
609
  private MarkdownEditorPane getActiveEditor() {
610
    final EditorPane pane = getActiveFileEditor().getEditorPane();
611
612
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane)pane : null;
613
  }
614
615
  private FileEditorTab getActiveFileEditor() {
616
    return getFileEditorPane().getActiveFileEditor();
617
  }
618
619
  //---- Member accessors ---------------------------------------------------
620
  private void setProcessors( final Map<FileEditorTab, Processor<String>> map ) {
621
    this.processors = map;
622
  }
623
624
  private Map<FileEditorTab, Processor<String>> getProcessors() {
625
    if( this.processors == null ) {
626
      setProcessors( new HashMap<>() );
627
    }
628
629
    return this.processors;
630
  }
631
632
  private FileEditorTabPane getFileEditorPane() {
633
    if( this.fileEditorPane == null ) {
634
      this.fileEditorPane = createFileEditorPane();
635
    }
636
637
    return this.fileEditorPane;
638
  }
639
640
  private HTMLPreviewPane getPreviewPane() {
641
    if( this.previewPane == null ) {
642
      this.previewPane = createPreviewPane();
643
    }
644
645
    return this.previewPane;
646
  }
647
648
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
649
    this.definitionSource = definitionSource;
650
  }
651
652
  private DefinitionSource getDefinitionSource() {
653
    if( this.definitionSource == null ) {
654
      this.definitionSource = new EmptyDefinitionSource();
655
    }
656
657
    return this.definitionSource;
658
  }
659
660
  private DefinitionPane getDefinitionPane() {
661
    if( this.definitionPane == null ) {
662
      this.definitionPane = createDefinitionPane();
663
    }
664
665
    return this.definitionPane;
666
  }
667
668
  private Options getOptions() {
669
    return this.options;
670
  }
671
672
  private Snitch getSnitch() {
673
    return this.snitch;
674
  }
675
676
  private Notifier getNotifier() {
677
    return this.notifier;
678
  }
679
680
  public void setMenuBar( final MenuBar menuBar ) {
681
    this.menuBar = menuBar;
682
  }
683
684
  public MenuBar getMenuBar() {
685
    return this.menuBar;
686
  }
687
688
  private Text getLineNumberText() {
689
    if( this.lineNumberText == null ) {
690
      this.lineNumberText = createLineNumberText();
691
    }
692
693
    return this.lineNumberText;
694
  }
695
696
  private synchronized StatusBar getStatusBar() {
697
    if( this.statusBar == null ) {
698
      this.statusBar = createStatusBar();
699
    }
700
701
    return this.statusBar;
702
  }
703
704
  private TextField getFindTextField() {
705
    if( this.findTextField == null ) {
706
      this.findTextField = createFindTextField();
707
    }
708
709
    return this.findTextField;
710
  }
711
712
  //---- Member creators ----------------------------------------------------
713
  /**
714
   * Factory to create processors that are suited to different file types.
715
   *
716
   * @param tab The tab that is subjected to processing.
717
   *
718
   * @return A processor suited to the file type specified by the tab's path.
719
   */
720
  private Processor<String> createProcessor( final FileEditorTab tab ) {
721
    return createProcessorFactory().createProcessor( tab );
722
  }
723
724
  private ProcessorFactory createProcessorFactory() {
725
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
726
  }
727
728
  private DefinitionSource createDefinitionSource( final String path ) {
729
    DefinitionSource ds;
730
731
    try {
732
       ds = createDefinitionFactory().createDefinitionSource( path );
733
734
      if( ds instanceof FileDefinitionSource ) {
735
        try {
736
          getSnitch().listen( ((FileDefinitionSource)ds).getPath() );
737
        } catch( final IOException ex ) {
738
          error( ex );
739
        }
740
      }
741
    } catch( final Exception ex ) {
742
      ds = new EmptyDefinitionSource();
743
      error( ex );
744
    }
745
746
    return ds;
747
  }
748
749
  private TextField createFindTextField() {
750
    return new TextField();
751
  }
752
753
  /**
754
   * Create an editor pane to hold file editor tabs.
755
   *
756
   * @return A new instance, never null.
757
   */
758
  private FileEditorTabPane createFileEditorPane() {
759
    return new FileEditorTabPane();
760
  }
761
762
  private HTMLPreviewPane createPreviewPane() {
763
    return new HTMLPreviewPane();
764
  }
765
766
  private DefinitionPane createDefinitionPane() {
767
    return new DefinitionPane( getTreeView() );
768
  }
769
770
  private DefinitionFactory createDefinitionFactory() {
771
    return new DefinitionFactory();
772
  }
773
774
  private StatusBar createStatusBar() {
775
    return new StatusBar();
776
  }
777
778
  private Scene createScene() {
779
    final SplitPane splitPane = new SplitPane(
780
      getDefinitionPane().getNode(),
781
      getFileEditorPane().getNode(),
782
      getPreviewPane().getNode() );
783
784
    splitPane.setDividerPositions(
785
      getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
786
      getFloat( K_PANE_SPLIT_EDITOR, .45f ),
787
      getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
788
789
    // See: http://broadlyapplicable.blogspot.ca/2015/03/javafx-capture-restorePreferences-splitpane.html
790
    final BorderPane borderPane = new BorderPane();
791
    borderPane.setPrefSize( 1024, 800 );
792
    borderPane.setTop( createMenuBar() );
793
    borderPane.setBottom( getStatusBar() );
794
    borderPane.setCenter( splitPane );
795
796
    final VBox box = new VBox();
797
    box.setAlignment( Pos.BASELINE_CENTER );
798
    box.getChildren().add( getLineNumberText() );
799
    getStatusBar().getRightItems().add( box );
800
801
    return new Scene( borderPane );
802
  }
803
804
  private Text createLineNumberText() {
805
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
806
  }
807
808
  private Node createMenuBar() {
809
    final BooleanBinding activeFileEditorIsNull = getFileEditorPane().activeFileEditorProperty().isNull();
810
811
    // File actions
812
    final Action fileNewAction = new Action( get( "Main.menu.file.new" ), "Shortcut+N", FILE_ALT, e -> fileNew() );
813
    final Action fileOpenAction = new Action( get( "Main.menu.file.open" ), "Shortcut+O", FOLDER_OPEN_ALT, e -> fileOpen() );
814
    final Action fileCloseAction = new Action( get( "Main.menu.file.close" ), "Shortcut+W", null, e -> fileClose(), activeFileEditorIsNull );
815
    final Action fileCloseAllAction = new Action( get( "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(), activeFileEditorIsNull );
816
    final Action fileSaveAction = new Action( get( "Main.menu.file.save" ), "Shortcut+S", FLOPPY_ALT, e -> fileSave(),
817
      createActiveBooleanProperty( FileEditorTab::modifiedProperty ).not() );
818
    final Action fileSaveAsAction = new Action( Messages.get( "Main.menu.file.save_as" ), null, null, e -> fileSaveAs(), activeFileEditorIsNull );
819
    final Action fileSaveAllAction = new Action( get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null, e -> fileSaveAll(),
820
      Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
821
    final Action fileExitAction = new Action( get( "Main.menu.file.exit" ), null, null, e -> fileExit() );
822
823
    // Edit actions
824
    final Action editUndoAction = new Action( get( "Main.menu.edit.undo" ), "Shortcut+Z", UNDO,
825
      e -> getActiveEditor().undo(),
826
      createActiveBooleanProperty( FileEditorTab::canUndoProperty ).not() );
827
    final Action editRedoAction = new Action( get( "Main.menu.edit.redo" ), "Shortcut+Y", REPEAT,
828
      e -> getActiveEditor().redo(),
829
      createActiveBooleanProperty( FileEditorTab::canRedoProperty ).not() );
830
    final Action editFindAction = new Action( Messages.get( "Main.menu.edit.find" ), "Ctrl+F", SEARCH,
831
      e -> find(),
832
      activeFileEditorIsNull );
833
    final Action editReplaceAction = new Action( Messages.get( "Main.menu.edit.find.replace" ), "Shortcut+H", RETWEET,
834
      e -> getActiveEditor().replace(),
835
      activeFileEditorIsNull );
836
    final Action editFindNextAction = new Action( Messages.get( "Main.menu.edit.find.next" ), "F3", null,
837
      e -> findNext(),
838
      activeFileEditorIsNull );
839
    final Action editFindPreviousAction = new Action( Messages.get( "Main.menu.edit.find.previous" ), "Shift+F3", null,
840
      e -> getActiveEditor().findPrevious(),
841
      activeFileEditorIsNull );
842
843
    // Insert actions
844
    final Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ), "Shortcut+B", BOLD,
845
      e -> getActiveEditor().surroundSelection( "**", "**" ),
846
      activeFileEditorIsNull );
847
    final Action insertItalicAction = new Action( get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
848
      e -> getActiveEditor().surroundSelection( "*", "*" ),
849
      activeFileEditorIsNull );
850
    final Action insertSuperscriptAction = new Action( get( "Main.menu.insert.superscript" ), "Shortcut+[", SUPERSCRIPT,
851
      e -> getActiveEditor().surroundSelection( "^", "^" ),
852
      activeFileEditorIsNull );
853
    final Action insertSubscriptAction = new Action( get( "Main.menu.insert.subscript" ), "Shortcut+]", SUBSCRIPT,
854
      e -> getActiveEditor().surroundSelection( "~", "~" ),
855
      activeFileEditorIsNull );
856
    final Action insertStrikethroughAction = new Action( get( "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
857
      e -> getActiveEditor().surroundSelection( "~~", "~~" ),
858
      activeFileEditorIsNull );
859
    final Action insertBlockquoteAction = new Action( get( "Main.menu.insert.blockquote" ), "Ctrl+Q", QUOTE_LEFT, // not Shortcut+Q because of conflict on Mac
860
      e -> getActiveEditor().surroundSelection( "\n\n> ", "" ),
861
      activeFileEditorIsNull );
862
    final Action insertCodeAction = new Action( get( "Main.menu.insert.code" ), "Shortcut+K", CODE,
863
      e -> getActiveEditor().surroundSelection( "`", "`" ),
864
      activeFileEditorIsNull );
865
    final Action insertFencedCodeBlockAction = new Action( get( "Main.menu.insert.fenced_code_block" ), "Shortcut+Shift+K", FILE_CODE_ALT,
866
      e -> getActiveEditor().surroundSelection( "\n\n```\n", "\n```\n\n", get( "Main.menu.insert.fenced_code_block.prompt" ) ),
867
      activeFileEditorIsNull );
868
869
    final Action insertLinkAction = new Action( get( "Main.menu.insert.link" ), "Shortcut+L", LINK,
870
      e -> getActiveEditor().insertLink(),
871
      activeFileEditorIsNull );
872
    final Action insertImageAction = new Action( get( "Main.menu.insert.image" ), "Shortcut+G", PICTURE_ALT,
873
      e -> getActiveEditor().insertImage(),
874
      activeFileEditorIsNull );
875
876
    final Action[] headers = new Action[ 6 ];
877
878
    // Insert header actions (H1 ... H6)
879
    for( int i = 1; i <= 6; i++ ) {
880
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
881
      final String markup = String.format( "%n%n%s ", hashes );
882
      final String text = get( "Main.menu.insert.header_" + i );
883
      final String accelerator = "Shortcut+" + i;
884
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
885
886
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
887
        e -> getActiveEditor().surroundSelection( markup, "", prompt ),
888
        activeFileEditorIsNull );
889
    }
890
891
    final Action insertUnorderedListAction = new Action(
892
      get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
893
      e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
894
      activeFileEditorIsNull );
895
    final Action insertOrderedListAction = new Action(
896
      get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
897
      e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
898
      activeFileEditorIsNull );
899
    final Action insertHorizontalRuleAction = new Action(
900
      get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
901
      e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
902
      activeFileEditorIsNull );
903
904
    // Tools actions
905
    final Action toolsScriptAction = new Action(
906
      get( "Main.menu.tools.script" ), null, null, e -> toolsScript() );
907
908
    // Help actions
909
    final Action helpAboutAction = new Action(
910
      get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
911
912
    //---- MenuBar ----
913
    final Menu fileMenu = ActionUtils.createMenu( get( "Main.menu.file" ),
914
      fileNewAction,
915
      fileOpenAction,
916
      null,
917
      fileCloseAction,
918
      fileCloseAllAction,
919
      null,
920
      fileSaveAction,
921
      fileSaveAsAction,
922
      fileSaveAllAction,
923
      null,
924
      fileExitAction );
925
926
    final Menu editMenu = ActionUtils.createMenu( get( "Main.menu.edit" ),
927
      editUndoAction,
928
      editRedoAction,
929
      editFindAction,
930
      editReplaceAction,
931
      editFindNextAction,
932
      editFindPreviousAction );
933
934
    final Menu insertMenu = ActionUtils.createMenu( get( "Main.menu.insert" ),
935
      insertBoldAction,
936
      insertItalicAction,
937
      insertSuperscriptAction,
938
      insertSubscriptAction,
939
      insertStrikethroughAction,
940
      insertBlockquoteAction,
941
      insertCodeAction,
942
      insertFencedCodeBlockAction,
943
      null,
944
      insertLinkAction,
945
      insertImageAction,
946
      null,
947
      headers[ 0 ],
948
      headers[ 1 ],
949
      headers[ 2 ],
950
      headers[ 3 ],
951
      headers[ 4 ],
952
      headers[ 5 ],
953
      null,
954
      insertUnorderedListAction,
955
      insertOrderedListAction,
956
      insertHorizontalRuleAction );
957
958
    final Menu toolsMenu = ActionUtils.createMenu( get( "Main.menu.tools" ),
959
      toolsScriptAction );
960
961
    final Menu helpMenu = ActionUtils.createMenu( get( "Main.menu.help" ),
962
      helpAboutAction );
963
964
    menuBar = new MenuBar( fileMenu, editMenu, insertMenu, toolsMenu, helpMenu );
965
966
    //---- ToolBar ----
967
    ToolBar toolBar = ActionUtils.createToolBar(
968
      fileNewAction,
969
      fileOpenAction,
970
      fileSaveAction,
971
      null,
972
      editUndoAction,
973
      editRedoAction,
974
      null,
975
      insertBoldAction,
976
      insertItalicAction,
977
      insertSuperscriptAction,
978
      insertSubscriptAction,
979
      insertBlockquoteAction,
980
      insertCodeAction,
981
      insertFencedCodeBlockAction,
982
      null,
983
      insertLinkAction,
984
      insertImageAction,
985
      null,
986
      headers[ 0 ],
987
      null,
988
      insertUnorderedListAction,
989
      insertOrderedListAction );
990
991
    return new VBox( menuBar, toolBar );
992
  }
993
994
  /**
995
   * Creates a boolean property that is bound to another boolean value of the
996
   * active editor.
997
   */
998
  private BooleanProperty createActiveBooleanProperty(
999
    final Function<FileEditorTab, ObservableBooleanValue> func ) {
1000
1001
    final BooleanProperty b = new SimpleBooleanProperty();
1002
    final FileEditorTab tab = getActiveFileEditor();
1003
1004
    if( tab != null ) {
1005
      b.bind( func.apply( tab ) );
1006
    }
1007
1008
    getFileEditorPane().activeFileEditorProperty().addListener(
1009
      (observable, oldFileEditor, newFileEditor) -> {
1010
        b.unbind();
1011
1012
        if( newFileEditor != null ) {
1013
          b.bind( func.apply( newFileEditor ) );
1014
        }
1015
        else {
1016
          b.set( false );
1017
        }
1018
      }
1019
    );
1020
1021
    return b;
1022
  }
1023
1024
  private void initLayout() {
1025
    final Scene appScene = getScene();
1026
1027
    appScene.getStylesheets().add( STYLESHEET_SCENE );
1028
1029
    // TODO: Apply an XML syntax highlighting for XML files.
1030
//    appScene.getStylesheets().add( STYLESHEET_XML );
1031
    appScene.windowProperty().addListener(
1032
      (observable, oldWindow, newWindow) -> {
1033
        newWindow.setOnCloseRequest( e -> {
1034
          if( !getFileEditorPane().closeAllEditors() ) {
1035
            e.consume();
1036
          }
1037
        } );
1038
1039
        // Workaround JavaFX bug: deselect menubar if window loses focus.
1040
        newWindow.focusedProperty().addListener(
1041
          (obs, oldFocused, newFocused) -> {
1042
            if( !newFocused ) {
1043
              // Send an ESC key event to the menubar
1044
              this.menuBar.fireEvent(
1045
                new KeyEvent(
1046
                  KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
1047
                  false, false, false, false ) );
1048
            }
1049
          }
1050
        );
1051
      }
1052
    );
1053
  }
1054
}
11055
A src/main/java/com/scrivenvar/Messages.java
1
/*
2
 * Copyright (c) 2016 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  * Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  * Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar;
28
29
import static com.scrivenvar.Constants.APP_BUNDLE_NAME;
30
import java.text.MessageFormat;
31
import java.util.ResourceBundle;
32
import java.util.Stack;
33
34
/**
35
 * Recursively resolves message properties. Property values can refer to other
36
 * properties using a <code>${var}</code> syntax.
37
 *
38
 * @author Karl Tauber, Dave Jarvis
39
 */
40
public class Messages {
41
42
  private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(APP_BUNDLE_NAME );
43
44
  private Messages() {
45
  }
46
47
  /**
48
   * Return the value of a resource bundle value after having resolved any
49
   * references to other bundle variables.
50
   *
51
   * @param props The bundle containing resolvable properties.
52
   * @param s The value for a key to resolve.
53
   *
54
   * @return The value of the key with all references recursively dereferenced.
55
   */
56
  private static String resolve( ResourceBundle props, String s ) {
57
    final int len = s.length();
58
    final Stack<StringBuilder> stack = new Stack<>();
59
60
    StringBuilder sb = new StringBuilder( 256 );
61
    boolean open = false;
62
63
    for( int i = 0; i < len; i++ ) {
64
      final char c = s.charAt( i );
65
66
      switch( c ) {
67
        case '$': {
68
          if( i + 1 < len && s.charAt( i + 1 ) == '{' ) {
69
            stack.push( sb );
70
            sb = new StringBuilder( 256 );
71
            i++;
72
            open = true;
73
          }
74
75
          break;
76
        }
77
78
        case '}': {
79
          if( open ) {
80
            open = false;
81
            final String name = sb.toString();
82
83
            sb = stack.pop();
84
            sb.append( props.getString( name ) );
85
            break;
86
          }
87
        }
88
89
        default: {
90
          sb.append( c );
91
          break;
92
        }
93
      }
94
    }
95
96
    if( open ) {
97
      throw new IllegalArgumentException( "missing '}'" );
98
    }
99
100
    return sb.toString();
101
  }
102
103
  /**
104
   * Returns the value for a key from the message bundle.
105
   *
106
   * @param key Retrieve the value for this key.
107
   *
108
   * @return The value for the key.
109
   */
110
  public static String get( String key ) {
111
    String result;
112
113
    try {
114
      result = resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
115
    } catch( final Exception ex ) {
116
      result = key;
117
    }
118
119
    return result;
120
  }
121
122
  /**
123
   * Returns the value for a key from the message bundle with the arguments
124
   * replacing <code>{#}</code> place holders.
125
   *
126
   * @param key Retrieve the value for this key.
127
   * @param args The values to substitute for place holders.
128
   *
129
   * @return The value for the key.
130
   */
131
  public static String get( String key, Object... args ) {
132
    return MessageFormat.format( get( key ), args );
133
  }
134
}
1135
A src/main/java/com/scrivenvar/Services.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
import java.util.ServiceLoader;
33
34
/**
35
 * Responsible for loading services. The services are treated as singleton
36
 * instances.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class Services {
41
42
  private static final Map<Class, Object> SINGLETONS = new HashMap<>();
43
44
  /**
45
   * Loads a service based on its interface definition. This will return an
46
   * existing instance if the class has already been instantiated.
47
   *
48
   * @param <T> The service to load.
49
   * @param api The interface definition for the service.
50
   *
51
   * @return A class that implements the interface.
52
   */
53
  public static <T> T load( final Class<T> api ) {
54
    @SuppressWarnings( "unchecked" )
55
    final T o = (T)get( api );
56
57
    return o == null ? newInstance( api ) : o;
58
  }
59
60
  private static <T> T newInstance( final Class<T> api ) {
61
    final ServiceLoader<T> services = ServiceLoader.load( api );
62
63
    for( final T service : services ) {
64
      if( service != null ) {
65
        // Re-use the same instance the next time the class is loaded.
66
        put( api, service );
67
        return service;
68
      }
69
    }
70
71
    throw new RuntimeException( "No implementation for: " + api );
72
  }
73
74
  private static void put( Class key, Object value ) {
75
    SINGLETONS.put( key, value );
76
  }
77
78
  private static Object get( Class api ) {
79
    return SINGLETONS.get( api );
80
  }
81
}
182
A src/main/java/com/scrivenvar/controls/BrowseDirectoryButton.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.scrivenvar.controls;
29
30
import java.io.File;
31
import javafx.event.ActionEvent;
32
import javafx.scene.control.Tooltip;
33
import javafx.stage.DirectoryChooser;
34
import com.scrivenvar.Messages;
35
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
36
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
37
38
/**
39
 * Button that opens a directory chooser to select a local directory for a URL in markdown.
40
 *
41
 * @author Karl Tauber
42
 */
43
public class BrowseDirectoryButton
44
	extends BrowseFileButton
45
{
46
	public BrowseDirectoryButton() {
47
		setGraphic(FontAwesomeIconFactory.get().createIcon(FontAwesomeIcon.FOLDER_ALT, "1.2em"));
48
		setTooltip(new Tooltip(Messages.get("BrowseDirectoryButton.tooltip")));
49
	}
50
51
	@Override
52
	protected void browse(ActionEvent e) {
53
		DirectoryChooser directoryChooser = new DirectoryChooser();
54
		directoryChooser.setTitle(Messages.get("BrowseDirectoryButton.chooser.title"));
55
		directoryChooser.setInitialDirectory(getInitialDirectory());
56
		File result = directoryChooser.showDialog(getScene().getWindow());
57
		if (result != null)
58
			updateUrl(result);
59
	}
60
}
161
A src/main/java/com/scrivenvar/controls/BrowseFileButton.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.scrivenvar.controls;
29
30
import java.io.File;
31
import java.nio.file.Path;
32
import java.util.ArrayList;
33
import java.util.List;
34
import javafx.beans.property.ObjectProperty;
35
import javafx.beans.property.SimpleObjectProperty;
36
import javafx.event.ActionEvent;
37
import javafx.scene.control.Button;
38
import javafx.scene.control.Tooltip;
39
import javafx.scene.input.KeyCode;
40
import javafx.scene.input.KeyEvent;
41
import javafx.stage.FileChooser;
42
import javafx.stage.FileChooser.ExtensionFilter;
43
import com.scrivenvar.Messages;
44
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
45
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
46
47
/**
48
 * Button that opens a file chooser to select a local file for a URL in markdown.
49
 *
50
 * @author Karl Tauber
51
 */
52
public class BrowseFileButton
53
	extends Button
54
{
55
	private final List<ExtensionFilter> extensionFilters = new ArrayList<>();
56
57
	public BrowseFileButton() {
58
		setGraphic(FontAwesomeIconFactory.get().createIcon(FontAwesomeIcon.FILE_ALT, "1.2em"));
59
		setTooltip(new Tooltip(Messages.get("BrowseFileButton.tooltip")));
60
		setOnAction(this::browse);
61
62
		disableProperty().bind(basePath.isNull());
63
64
		// workaround for a JavaFX bug:
65
		//   avoid closing the dialog that contains this control when the user
66
		//   closes the FileChooser or DirectoryChooser using the ESC key
67
		addEventHandler(KeyEvent.KEY_RELEASED, e-> {
68
			if (e.getCode() == KeyCode.ESCAPE)
69
				e.consume();
70
		});
71
	}
72
73
	public void addExtensionFilter(ExtensionFilter extensionFilter) {
74
		extensionFilters.add(extensionFilter);
75
	}
76
77
	// 'basePath' property
78
	private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>();
79
	public Path getBasePath() { return basePath.get(); }
80
	public void setBasePath(Path basePath) { this.basePath.set(basePath); }
81
	public ObjectProperty<Path> basePathProperty() { return basePath; }
82
83
	// 'url' property
84
	private final ObjectProperty<String> url = new SimpleObjectProperty<>();
85
	public String getUrl() { return url.get(); }
86
	public void setUrl(String url) { this.url.set(url); }
87
	public ObjectProperty<String> urlProperty() { return url; }
88
89
	protected void browse(ActionEvent e) {
90
		FileChooser fileChooser = new FileChooser();
91
		fileChooser.setTitle(Messages.get("BrowseFileButton.chooser.title"));
92
		fileChooser.getExtensionFilters().addAll(extensionFilters);
93
		fileChooser.getExtensionFilters().add(new ExtensionFilter(Messages.get("BrowseFileButton.chooser.allFilesFilter"), "*.*"));
94
		fileChooser.setInitialDirectory(getInitialDirectory());
95
		File result = fileChooser.showOpenDialog(getScene().getWindow());
96
		if (result != null)
97
			updateUrl(result);
98
	}
99
100
	protected File getInitialDirectory() {
101
		//TODO build initial directory based on current value of 'url' property
102
		return getBasePath().toFile();
103
	}
104
105
	protected void updateUrl(File file) {
106
		String newUrl;
107
		try {
108
			newUrl = getBasePath().relativize(file.toPath()).toString();
109
		} catch (IllegalArgumentException ex) {
110
			newUrl = file.toString();
111
		}
112
		url.set(newUrl.replace('\\', '/'));
113
	}
114
}
1115
A src/main/java/com/scrivenvar/controls/EscapeTextField.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.scrivenvar.controls;
29
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.scene.control.TextField;
33
import javafx.util.StringConverter;
34
import com.scrivenvar.util.Utils;
35
36
/**
37
 * TextField that can escape/unescape characters for markdown.
38
 *
39
 * @author Karl Tauber
40
 */
41
public class EscapeTextField
42
	extends TextField
43
{
44
	public EscapeTextField() {
45
		escapedText.bindBidirectional(textProperty(), new StringConverter<String>() {
46
			@Override public String toString(String object) { return escape(object); }
47
			@Override public String fromString(String string) { return unescape(string); }
48
		});
49
		escapeCharacters.addListener(e -> escapedText.set(escape(textProperty().get())));
50
	}
51
52
	// 'escapedText' property
53
	private final StringProperty escapedText = new SimpleStringProperty();
54
	public String getEscapedText() { return escapedText.get(); }
55
	public void setEscapedText(String escapedText) { this.escapedText.set(escapedText); }
56
	public StringProperty escapedTextProperty() { return escapedText; }
57
58
	// 'escapeCharacters' property
59
	private final StringProperty escapeCharacters = new SimpleStringProperty();
60
	public String getEscapeCharacters() { return escapeCharacters.get(); }
61
	public void setEscapeCharacters(String escapeCharacters) { this.escapeCharacters.set(escapeCharacters); }
62
	public StringProperty escapeCharactersProperty() { return escapeCharacters; }
63
64
	private String escape(String s) {
65
		String escapeChars = getEscapeCharacters();
66
		return !Utils.isNullOrEmpty(escapeChars)
67
				? s.replaceAll("([" + escapeChars.replaceAll("(.)", "\\\\$1") + "])", "\\\\$1")
68
				: s;
69
	}
70
71
	private String unescape(String s) {
72
		String escapeChars = getEscapeCharacters();
73
		return !Utils.isNullOrEmpty(escapeChars)
74
				? s.replaceAll("\\\\([" + escapeChars.replaceAll("(.)", "\\\\$1") + "])", "$1")
75
				: s;
76
	}
77
}
178
A src/main/java/com/scrivenvar/controls/FlagCheckBox.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.controls;
28
29
import javafx.beans.property.IntegerProperty;
30
import javafx.beans.property.SimpleIntegerProperty;
31
import javafx.scene.control.CheckBox;
32
33
/**
34
 * CheckBox that toggles a bit in an integer.
35
 *
36
 * @author Karl Tauber
37
 */
38
public class FlagCheckBox extends CheckBox {
39
40
  public FlagCheckBox() {
41
    setOnAction( e -> {
42
      if( isSelected() ) {
43
        setFlags( getFlags() | getFlag() );
44
      } else {
45
        setFlags( getFlags() & ~getFlag() );
46
      }
47
    } );
48
49
    flags.addListener( (obs, oldFlags, newFlags) -> {
50
      setSelected( (newFlags.intValue() & getFlag()) != 0 );
51
    } );
52
  }
53
54
  // 'flag' property
55
  private final IntegerProperty flag = new SimpleIntegerProperty();
56
57
  public int getFlag() {
58
    return flag.get();
59
  }
60
61
  public void setFlag( int flag ) {
62
    this.flag.set( flag );
63
  }
64
65
  public IntegerProperty flagProperty() {
66
    return flag;
67
  }
68
69
  // 'flags' property
70
  private final IntegerProperty flags = new SimpleIntegerProperty();
71
72
  public int getFlags() {
73
    return flags.get();
74
  }
75
76
  public void setFlags( int flags ) {
77
    this.flags.set( flags );
78
  }
79
80
  public IntegerProperty flagsProperty() {
81
    return flags;
82
  }
83
}
184
A src/main/java/com/scrivenvar/controls/WebHyperlink.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.controls;
28
29
import com.scrivenvar.Main;
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.scene.control.Hyperlink;
33
34
/**
35
 * Opens a web site in the default web browser.
36
 *
37
 * @author Karl Tauber
38
 */
39
public class WebHyperlink extends Hyperlink {
40
41
  // 'uri' property
42
  private final StringProperty uri = new SimpleStringProperty();
43
44
  public WebHyperlink() {
45
    setStyle( "-fx-padding: 0; -fx-border-width: 0" );
46
  }
47
48
  @Override
49
  public void fire() {
50
    Main.showDocument( getUri() );
51
  }
52
53
  public String getUri() {
54
    return uri.get();
55
  }
56
57
  public void setUri( String uri ) {
58
    this.uri.set( uri );
59
  }
60
61
  public StringProperty uriProperty() {
62
    return uri;
63
  }
64
}
165
A src/main/java/com/scrivenvar/decorators/RVariableDecorator.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.decorators;
29
30
/**
31
 * Brackets variable names with <code>`r#</code> and <code>`</code>.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class RVariableDecorator implements VariableDecorator {
36
  public static final String PREFIX = "`r#";
37
  public static final char SUFFIX = '`';
38
39
  /**
40
   * Returns the given string R-escaping backticks prepended and appended. This
41
   * is not null safe. Do not pass null into this method.
42
   *
43
   * @param variableName The string to decorate.
44
   *
45
   * @return "`r#" + variableName + "`".
46
   */
47
  @Override
48
  public String decorate( final String variableName ) {
49
    // 12 = PREFIX + x(...) + SUFFIX + 1 for good measure
50
    final StringBuilder sb = new StringBuilder( variableName.length() + 12 );
51
    
52
    sb.append( PREFIX );
53
    sb.append( "x( v$" );
54
    sb.append( variableName.replace( '.', '$' ) );
55
    sb.append( " )" );
56
    sb.append( SUFFIX );
57
    
58
    return sb.toString();
59
  }
60
}
161
A src/main/java/com/scrivenvar/decorators/VariableDecorator.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.decorators;
29
30
/**
31
 * Responsible for updating variable names to use a machine-readable format
32
 * corresponding to the type of file being edited.
33
 */
34
public interface VariableDecorator {
35
36
  /**
37
   * This decorates a variable name based on some criteria determined by the
38
   * factory that creates implementations of this interface.
39
   *
40
   * @param variableName The text to decorate as per the filename extension
41
   * would indicate (e.g., ".md" goes to $VAR$ while ".Rmd" goes to `r#VAR`).
42
   *
43
   * @return The given variable name modified with its requisite delimiters.
44
   */
45
  public String decorate( String variableName );
46
}
147
A src/main/java/com/scrivenvar/decorators/YamlVariableDecorator.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.decorators;
29
30
/**
31
 * Brackets variable names with dollar symbols.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class YamlVariableDecorator implements VariableDecorator {
36
37
  /**
38
   * Matches variables delimited by dollar symbols. The outer group is necessary
39
   * for substring replacement of delimited references.
40
   */
41
  public final static String REGEX = "(\\$(.*?)\\$)";
42
43
  /**
44
   * Returns the given string with a $ symbol prepended and appended. This is
45
   * not null safe. Do not pass null into this method.
46
   *
47
   * @param variableName The string to decorate.
48
   *
49
   * @return '$' + variableName + '$';
50
   */
51
  @Override
52
  public String decorate( final String variableName ) {
53
    return '$' + variableName + '$';
54
  }
55
}
156
A src/main/java/com/scrivenvar/definition/AbstractDefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.scene.control.TreeView;
31
32
/**
33
 * Implements common behaviour for definition sources.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public abstract class AbstractDefinitionSource implements DefinitionSource {
38
39
  private TreeView<String> treeView;
40
41
  /**
42
   * Returns this definition source as an editable graphical user interface
43
   * component.
44
   *
45
   * @return The TreeView for this definition source.
46
   */
47
  @Override
48
  public TreeView<String> asTreeView() {
49
50
    if( this.treeView == null ) {
51
      this.treeView = createTreeView();
52
      this.treeView.setEditable( true );
53
      this.treeView.setCellFactory(
54
        (TreeView<String> t) -> new TextFieldTreeCell()
55
      );
56
    }
57
58
    return this.treeView;
59
  }
60
61
  /**
62
   * Creates a newly instantiated tree view ready for adding to the definition
63
   * pane.
64
   *
65
   * @return A new tree view instance, never null.
66
   */
67
  protected abstract TreeView<String> createTreeView();
68
}
169
A src/main/java/com/scrivenvar/definition/DefinitionFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import static com.scrivenvar.Constants.DEFINITION_PROTOCOL_FILE;
32
import static com.scrivenvar.Constants.DEFINITION_PROTOCOL_UNKNOWN;
33
import static com.scrivenvar.Constants.GLOB_PREFIX_DEFINITION;
34
import com.scrivenvar.FileType;
35
import static com.scrivenvar.FileType.YAML;
36
import com.scrivenvar.definition.yaml.YamlFileDefinitionSource;
37
import java.io.File;
38
import java.net.MalformedURLException;
39
import java.net.URI;
40
import java.net.URISyntaxException;
41
import java.net.URL;
42
import java.nio.file.Path;
43
import java.nio.file.Paths;
44
45
/**
46
 * Responsible for creating objects that can read and write definition data
47
 * sources. The data source could be YAML, TOML, JSON, flat files, or from a
48
 * database.
49
 *
50
 * @author White Magic Software, Ltd.
51
 */
52
public class DefinitionFactory extends AbstractFileFactory {
53
54
  /**
55
   * Default (empty) constructor.
56
   */
57
  public DefinitionFactory() {
58
  }
59
60
  /**
61
   * Creates a definition source capable of reading definitions from the given
62
   * path.
63
   *
64
   * @param path Path to a resource containing definitions.
65
   *
66
   * @return The definition source appropriate for the given path.
67
   */
68
  public DefinitionSource createDefinitionSource( final String path ) {
69
    final String protocol = getProtocol( path );
70
    DefinitionSource result = null;
71
72
    switch( protocol ) {
73
      case DEFINITION_PROTOCOL_FILE:
74
        final Path file = Paths.get( path );
75
        final FileType filetype = lookup( file, GLOB_PREFIX_DEFINITION );
76
        result = createFileDefinitionSource( filetype, file );
77
        break;
78
79
      default:
80
        unknownFileType( protocol, path );
81
        break;
82
    }
83
84
    return result;
85
  }
86
87
  /**
88
   * Creates a definition source based on the file type.
89
   *
90
   * @param filetype Property key name suffix from settings.properties file.
91
   * @param path Path to the file that corresponds to the extension.
92
   *
93
   * @return A DefinitionSource capable of parsing the data stored at the path.
94
   */
95
  private DefinitionSource createFileDefinitionSource(
96
    final FileType filetype, final Path path ) {
97
98
    DefinitionSource result = null;
99
100
    switch( filetype ) {
101
      case YAML:
102
        result = new YamlFileDefinitionSource( path );
103
        break;
104
105
      default:
106
        unknownFileType( filetype.toString(), path.toString() );
107
        break;
108
    }
109
110
    return result;
111
  }
112
113
  /**
114
   * Returns the protocol for a given URI or filename.
115
   *
116
   * @param source Determine the protocol for this URI or filename.
117
   *
118
   * @return The protocol for the given source.
119
   */
120
  private String getProtocol( final String source ) {
121
    String protocol = null;
122
123
    try {
124
      final URI uri = new URI( source );
125
126
      if( uri.isAbsolute() ) {
127
        protocol = uri.getScheme();
128
      }
129
      else {
130
        final URL url = new URL( source );
131
        protocol = url.getProtocol();
132
      }
133
    } catch( final URISyntaxException | MalformedURLException e ) {
134
      // Could be HTTP, HTTPS?
135
      if( source.startsWith( "//" ) ) {
136
        throw new IllegalArgumentException( "Relative context: " + source );
137
      }
138
      else {
139
        final File file = new File( source );
140
        protocol = getProtocol( file );
141
      }
142
    }
143
144
    return protocol;
145
  }
146
147
  /**
148
   * Returns the protocol for a given file.
149
   *
150
   * @param file Determine the protocol for this file.
151
   *
152
   * @return The protocol for the given file.
153
   */
154
  private String getProtocol( final File file ) {
155
    String result;
156
157
    try {
158
      result = file.toURI().toURL().getProtocol();
159
    } catch( final Exception e ) {
160
      result = DEFINITION_PROTOCOL_UNKNOWN;
161
    }
162
163
    return result;
164
  }
165
}
1166
A src/main/java/com/scrivenvar/definition/DefinitionPane.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import com.scrivenvar.AbstractPane;
31
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR_CHAR;
32
import com.scrivenvar.predicates.strings.ContainsPredicate;
33
import com.scrivenvar.predicates.strings.StartsPredicate;
34
import com.scrivenvar.predicates.strings.StringPredicate;
35
import static com.scrivenvar.util.Lists.getFirst;
36
import java.util.List;
37
import javafx.collections.ObservableList;
38
import javafx.scene.Node;
39
import javafx.scene.control.MultipleSelectionModel;
40
import javafx.scene.control.SelectionMode;
41
import javafx.scene.control.TreeItem;
42
import javafx.scene.control.TreeView;
43
44
/**
45
 * Provides a list of variables that can be referenced in the editor.
46
 *
47
 * @author White Magic Software, Ltd.
48
 */
49
public class DefinitionPane extends AbstractPane {
50
51
  /**
52
   * Trimmed off the end of a word to match a variable name.
53
   */
54
  private final static String TERMINALS = ":;,.!?-/\\¡¿";
55
56
  private TreeView<String> treeView;
57
58
  /**
59
   * Constructs a definition pane with a given tree view root.
60
   *
61
   * @see YamlTreeAdapter.adapt
62
   * @param root The root of the variable definition tree.
63
   */
64
  public DefinitionPane( final TreeView<String> root ) {
65
    setTreeView( root );
66
    initTreeView();
67
  }
68
69
  /**
70
   * Changes the root node of the tree view. Swaps the current root node for the
71
   * root node of the given
72
   *
73
   * @param treeView The tree view containing a new root node; if the parameter
74
   * is null, the tree is cleared.
75
   */
76
  public void setRoot( final TreeView<String> treeView ) {
77
    getTreeView().setRoot( treeView == null ? null : treeView.getRoot() );
78
  }
79
80
  /**
81
   * Clears the tree view by setting the root node to null.
82
   */
83
  public void clear() {
84
    setRoot( null );
85
  }
86
87
  /**
88
   * Finds a tree item with a value that exactly matches the given word.
89
   *
90
   * @param trunk The root item containing a list of nodes to search.
91
   * @param word The value of the item to find.
92
   * @param predicate Helps determine whether the node value matches the word.
93
   *
94
   * @return The item that matches the given word, or null if not found.
95
   */
96
  private TreeItem<String> findNode(
97
    final TreeItem<String> trunk,
98
    final StringPredicate predicate ) {
99
    TreeItem<String> result = null;
100
101
    if( trunk != null ) {
102
      final List<TreeItem<String>> branches = trunk.getChildren();
103
104
      for( final TreeItem<String> leaf : branches ) {
105
        if( predicate.test( leaf.getValue() ) ) {
106
          result = leaf;
107
          break;
108
        }
109
      }
110
    }
111
112
    return result;
113
  }
114
115
  /**
116
   * Calls findNode with the EqualsPredicate.
117
   *
118
   * @see findNode( TreeItem, String, Predicate )
119
   * @return The result from findNode.
120
   */
121
  private TreeItem<String> findStartsNode(
122
    final TreeItem<String> trunk,
123
    final String word ) {
124
    return findNode( trunk, new StartsPredicate( word ) );
125
  }
126
127
  /**
128
   * Calls findNode with the ContainsPredicate.
129
   *
130
   * @see findNode( TreeItem, String, Predicate )
131
   * @return The result from findNode.
132
   */
133
  private TreeItem<String> findSubstringNode(
134
    final TreeItem<String> trunk,
135
    final String word ) {
136
    return findNode( trunk, new ContainsPredicate( word ) );
137
  }
138
139
  /**
140
   * Finds a node that matches a prefix and suffix specified by the given path
141
   * variable. The prefix must match a valid node value. The suffix refers to
142
   * the start of a string that matches zero or more children of the node
143
   * specified by the prefix. The algorithm has the following cases:
144
   *
145
   * <ol>
146
   * <li>Path is empty, return first child.</li>
147
   * <li>Path contains a complete match, return corresponding node.</li>
148
   * <li>Path contains a partial match, return nearest node.</li>
149
   * <li>Path contains a complete and partial match, return nearest node.</li>
150
   * </ol>
151
   *
152
   * @param word The word typed by the user, which contains dot-separated node
153
   * names that represent a path within the YAML tree plus a partial variable
154
   * name match (for a node).
155
   *
156
   * @return The node value that starts with the suffix portion of the given
157
   * path, never null.
158
   */
159
  public TreeItem<String> findNode( final String word ) {
160
    String path = word;
161
162
    TreeItem<String> cItem = getTreeRoot();
163
    TreeItem<String> pItem = cItem;
164
165
    int index = path.indexOf( SEPARATOR_CHAR );
166
167
    while( index >= 0 ) {
168
      final String node = path.substring( 0, index );
169
      path = path.substring( index + 1 );
170
171
      if( (cItem = findStartsNode( cItem, node )) == null ) {
172
        break;
173
      }
174
175
      index = path.indexOf( SEPARATOR_CHAR );
176
      pItem = cItem;
177
    }
178
179
    // Find the node that starts with whatever the user typed.
180
    cItem = findStartsNode( pItem, path );
181
182
    // If there was no matching node, then find a substring match.
183
    if( cItem == null ) {
184
      cItem = findSubstringNode( pItem, path );
185
    }
186
187
    // If neither starts with nor substring matched a node, revert to the last
188
    // known valid node.
189
    if( cItem == null ) {
190
      cItem = pItem;
191
    }
192
193
    return sanitize( cItem );
194
  }
195
196
  /**
197
   * Returns the leaf that matches the given value. If the value is terminally
198
   * punctuated, the punctuation is removed if no match was found.
199
   *
200
   * @param value The value to find, never null.
201
   *
202
   * @return The leaf that contains the given value, or null if neither the
203
   * original value nor the terminally-trimmed value was found.
204
   */
205
  public VariableTreeItem<String> findLeaf( final String value ) {
206
    return findLeaf( value, false );
207
  }
208
209
  /**
210
   * Returns the leaf that matches the given value. If the value is terminally
211
   * punctuated, the punctuation is removed if no match was found.
212
   *
213
   * @param value The value to find, never null.
214
   * @param contains Set to true to perform a substring match if starts with
215
   * fails to match.
216
   *
217
   * @return The leaf that contains the given value, or null if neither the
218
   * original value nor the terminally-trimmed value was found.
219
   */
220
  public VariableTreeItem<String> findLeaf(
221
    final String value,
222
    final boolean contains ) {
223
224
    final VariableTreeItem<String> root = getTreeRoot();
225
    final VariableTreeItem<String> leaf = root.findLeaf( value, contains );
226
227
    return leaf == null
228
      ? root.findLeaf( rtrimTerminalPunctuation( value ) )
229
      : leaf;
230
  }
231
232
  /**
233
   * Removes punctuation from the end of a string. The character set includes:
234
   * <code>:;,.!?-/\¡¿</code>.
235
   *
236
   * @param s The string to trim, never null.
237
   *
238
   * @return The string trimmed of all terminal characters from the end
239
   */
240
  private String rtrimTerminalPunctuation( final String s ) {
241
    final StringBuilder result = new StringBuilder( s.trim() );
242
243
    while( TERMINALS.contains( "" + result.charAt( result.length() - 1 ) ) ) {
244
      result.setLength( result.length() - 1 );
245
    }
246
247
    return result.toString();
248
  }
249
250
  /**
251
   * Returns the tree root if either item or its first child are null.
252
   *
253
   * @param item The item to make null safe.
254
   *
255
   * @return A non-null TreeItem, possibly the root item (to avoid null).
256
   */
257
  private TreeItem<String> sanitize( final TreeItem<String> item ) {
258
    TreeItem<String> result;
259
260
    if( item == null ) {
261
      result = getTreeRoot();
262
    }
263
    else {
264
      result = item == getTreeRoot()
265
        ? getFirst( item.getChildren() )
266
        : item;
267
    }
268
269
    return result;
270
  }
271
272
  /**
273
   * Expands the node to the root, recursively.
274
   *
275
   * @param <T> The type of tree item to expand (usually String).
276
   * @param node The node to expand.
277
   */
278
  public <T> void expand( final TreeItem<T> node ) {
279
    if( node != null ) {
280
      expand( node.getParent() );
281
282
      if( !node.isLeaf() ) {
283
        node.setExpanded( true );
284
      }
285
    }
286
  }
287
288
  public void select( final TreeItem<String> item ) {
289
    clearSelection();
290
    selectItem( getTreeView().getRow( item ) );
291
  }
292
293
  private void clearSelection() {
294
    getSelectionModel().clearSelection();
295
  }
296
297
  private void selectItem( final int row ) {
298
    getSelectionModel().select( row );
299
  }
300
301
  /**
302
   * Collapses the tree, recursively.
303
   */
304
  public void collapse() {
305
    collapse( getTreeRoot().getChildren() );
306
  }
307
308
  /**
309
   * Collapses the tree, recursively.
310
   *
311
   * @param <T> The type of tree item to expand (usually String).
312
   * @param node The nodes to collapse.
313
   */
314
  private <T> void collapse( ObservableList<TreeItem<T>> nodes ) {
315
    for( final TreeItem<T> node : nodes ) {
316
      node.setExpanded( false );
317
      collapse( node.getChildren() );
318
    }
319
  }
320
321
  private void initTreeView() {
322
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
323
  }
324
325
  /**
326
   * Returns the root node to the tree view.
327
   *
328
   * @return getTreeView()
329
   */
330
  public Node getNode() {
331
    return getTreeView();
332
  }
333
334
  private MultipleSelectionModel getSelectionModel() {
335
    return getTreeView().getSelectionModel();
336
  }
337
338
  /**
339
   * Returns the tree view that contains the YAML definition hierarchy.
340
   *
341
   * @return A non-null instance.
342
   */
343
  private TreeView<String> getTreeView() {
344
    return this.treeView;
345
  }
346
347
  /**
348
   * Returns the root of the tree.
349
   *
350
   * @return The first node added to the YAML definition tree.
351
   */
352
  private VariableTreeItem<String> getTreeRoot() {
353
    final TreeItem<String> root = getTreeView().getRoot();
354
355
    return root instanceof VariableTreeItem ? (VariableTreeItem<String>)root : null;
356
  }
357
358
  public <T> boolean isRoot( final TreeItem<T> item ) {
359
    return getTreeRoot().equals( item );
360
  }
361
362
  /**
363
   * Sets the tree view (called by the constructor).
364
   *
365
   * @param treeView
366
   */
367
  private void setTreeView( final TreeView<String> treeView ) {
368
    if( treeView != null ) {
369
      this.treeView = treeView;
370
    }
371
  }
372
}
1373
A src/main/java/com/scrivenvar/definition/DefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import java.util.Map;
31
import javafx.scene.control.TreeView;
32
33
/**
34
 * Represents behaviours for reading and writing variable definitions.
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public interface DefinitionSource {
39
40
  /**
41
   * Creates a TreeView from this definition source. The definition source is
42
   * responsible for observing the TreeView instance for changes and persisting
43
   * them, if needed.
44
   *
45
   * @return A hierarchical tree suitable for displaying in the definition pane.
46
   */
47
  public TreeView<String> asTreeView();
48
49
  /**
50
   * Returns all the strings with their values resolved in a flat hierarchy.
51
   * This copies all the keys and resolved values into a new map.
52
   *
53
   * @return The new map created with all values having been resolved,
54
   * recursively.
55
   */
56
  public Map<String, String> getResolvedMap();
57
58
  /**
59
   * Must return a re-loadable path to the data source. For a file, this is the
60
   * absolute file path. For a database, this could be the JDBC connection. For
61
   * a web site, this might be the GET URL.
62
   *
63
   * @return A non-null, non-empty string.
64
   */
65
  @Override
66
  public String toString();
67
}
168
A src/main/java/com/scrivenvar/definition/EmptyDefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
import javafx.scene.control.TreeView;
33
34
/**
35
 * Creates a definition source that has no information to load or save.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public class EmptyDefinitionSource extends AbstractDefinitionSource {
40
41
  public EmptyDefinitionSource() {
42
  }
43
44
45
  @Override
46
  public Map<String, String> getResolvedMap() {
47
    return new HashMap<>();
48
  }
49
50
  @Override
51
  protected TreeView<String> createTreeView() {
52
    return new TreeView<>();
53
  }
54
}
155
A src/main/java/com/scrivenvar/definition/FileDefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import java.nio.file.Path;
31
32
/**
33
 * Implements common behaviour for file definition sources.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public abstract class FileDefinitionSource extends AbstractDefinitionSource {
38
39
  private Path path;
40
41
  /**
42
   * Constructs a new file definition source that can read and write data in the
43
   * hierarchical format contained within the file location specified by the
44
   * path.
45
   *
46
   * @param path Must not be null.
47
   */
48
  public FileDefinitionSource( final Path path ) {
49
    setPath( path );
50
  }
51
52
  private void setPath( final Path path ) {
53
    this.path = path;
54
  }
55
56
  public Path getPath() {
57
    return this.path;
58
  }
59
60
  /**
61
   * Returns the path represented by this object.
62
   *
63
   * @return The
64
   */
65
  @Override
66
  public String toString() {
67
    return getPath().toString();
68
  }
69
}
170
A src/main/java/com/scrivenvar/definition/TextFieldTreeCell.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import static com.scrivenvar.Messages.get;
31
import javafx.event.ActionEvent;
32
import javafx.scene.control.ContextMenu;
33
import javafx.scene.control.MenuItem;
34
import javafx.scene.control.TextField;
35
import javafx.scene.control.TreeCell;
36
import javafx.scene.control.TreeItem;
37
import static javafx.scene.input.KeyCode.ENTER;
38
import static javafx.scene.input.KeyCode.ESCAPE;
39
import javafx.scene.input.KeyEvent;
40
41
/**
42
 * Provides behaviour of adding, removing, and editing tree view items.
43
 *
44
 * @author White Magic Software, Ltd.
45
 */
46
public class TextFieldTreeCell extends TreeCell<String> {
47
48
  private TextField textField;
49
  private final ContextMenu editMenu = new ContextMenu();
50
51
  public TextFieldTreeCell() {
52
    initEditMenu();
53
  }
54
55
  private void initEditMenu() {
56
    final MenuItem addItem = createMenuItem( "Definition.menu.add" );
57
    final MenuItem removeItem = createMenuItem( "Definition.menu.remove" );
58
59
    addItem.setOnAction( (ActionEvent e) -> {
60
      final VariableTreeItem<String> treeItem = new VariableTreeItem<>( "Undefined" );
61
      getTreeItem().getChildren().add( treeItem );
62
    } );
63
64
    removeItem.setOnAction( (ActionEvent e) -> {
65
      final TreeItem c = getTreeItem();
66
      c.getParent().getChildren().remove( c );
67
    } );
68
69
    getEditMenu().getItems().add( addItem );
70
    getEditMenu().getItems().add( removeItem );
71
  }
72
73
  private ContextMenu getEditMenu() {
74
    return this.editMenu;
75
  }
76
77
  private MenuItem createMenuItem( String label ) {
78
    return new MenuItem( get( label ) );
79
  }
80
81
  @Override
82
  public void startEdit() {
83
    if( getTreeItem().isLeaf() ) {
84
      super.startEdit();
85
86
      final TextField inputField = getTextField();
87
88
      setText( null );
89
      setGraphic( inputField );
90
      inputField.selectAll();
91
      inputField.requestFocus();
92
    }
93
  }
94
95
  @Override
96
  public void cancelEdit() {
97
    super.cancelEdit();
98
99
    setText( (String)getItem() );
100
    setGraphic( getTreeItem().getGraphic() );
101
  }
102
103
  @Override
104
  public void updateItem( String item, boolean empty ) {
105
    super.updateItem( item, empty );
106
107
    if( empty ) {
108
      setText( null );
109
      setGraphic( null );
110
    }
111
    else if( isEditing() ) {
112
      TextField tf = getTextField();
113
      tf.setText( getItemValue() );
114
115
      setText( null );
116
      setGraphic( tf );
117
    }
118
    else {
119
      setText( getItemValue() );
120
      setGraphic( getTreeItem().getGraphic() );
121
122
      if( !getTreeItem().isLeaf() && getTreeItem().getParent() != null ) {
123
        setContextMenu( getEditMenu() );
124
      }
125
    }
126
  }
127
128
  private TextField createTextField() {
129
    final TextField tf = new TextField( getItemValue() );
130
131
    tf.setOnKeyReleased( (KeyEvent t) -> {
132
      switch( t.getCode() ) {
133
        case ENTER:
134
          commitEdit( tf.getText() );
135
          break;
136
        case ESCAPE:
137
          cancelEdit();
138
          break;
139
      }
140
    } );
141
142
    return tf;
143
  }
144
145
  /**
146
   * Returns the item's text value.
147
   *
148
   * @return A non-null String, possibly empty.
149
   */
150
  private String getItemValue() {
151
    return getItem() == null ? "" : getItem();
152
  }
153
154
  private TextField getTextField() {
155
    if( this.textField == null ) {
156
      this.textField = createTextField();
157
    }
158
159
    return this.textField;
160
  }
161
}
1162
A src/main/java/com/scrivenvar/definition/VariableTreeItem.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import com.scrivenvar.decorators.VariableDecorator;
31
import com.scrivenvar.decorators.YamlVariableDecorator;
32
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR;
33
import static com.scrivenvar.editors.VariableNameInjector.DEFAULT_MAX_VAR_LENGTH;
34
import java.util.HashMap;
35
import java.util.Map;
36
import java.util.Stack;
37
import javafx.scene.control.TreeItem;
38
39
/**
40
 * Provides behaviour afforded to variable names and their corresponding value.
41
 *
42
 * @author White Magic Software, Ltd.
43
 * @param <T> The type of TreeItem (usually String).
44
 */
45
public class VariableTreeItem<T> extends TreeItem<T> {
46
47
  private final static int DEFAULT_MAP_SIZE = 1000;
48
49
  private final static VariableDecorator VARIABLE_DECORATOR
50
    = new YamlVariableDecorator();
51
52
  /**
53
   * Flattened tree.
54
   */
55
  private Map<String, String> map;
56
57
  /**
58
   * Constructs a new item with a default value.
59
   *
60
   * @param value Passed up to superclass.
61
   */
62
  public VariableTreeItem( final T value ) {
63
    super( value );
64
  }
65
66
  /**
67
   * Finds a leaf starting at the current node with text that matches the given
68
   * value.
69
   *
70
   * @param text The text to match against each leaf in the tree.
71
   *
72
   * @return The leaf that has a value starting with the given text.
73
   */
74
  public VariableTreeItem<T> findLeaf( final String text ) {
75
    return findLeaf( text, false );
76
  }
77
78
  /**
79
   * Finds a leaf starting at the current node with text that matches the given
80
   * value.
81
   *
82
   * @param text The text to match against each leaf in the tree.
83
   * @param contains Set to true to perform a substring match if starts with
84
   * fails.
85
   *
86
   * @return The leaf that has a value starting with the given text.
87
   */
88
  public VariableTreeItem<T> findLeaf(
89
    final String text,
90
    final boolean contains ) {
91
92
    final Stack<VariableTreeItem<T>> stack = new Stack<>();
93
    final VariableTreeItem<T> root = this;
94
95
    stack.push( root );
96
97
    boolean found = false;
98
    VariableTreeItem<T> node = null;
99
100
    while( !found && !stack.isEmpty() ) {
101
      node = stack.pop();
102
103
      if( contains && node.valueContains( text ) ) {
104
        found = true;
105
      }
106
      else if( !contains && node.valueStartsWith( text ) ) {
107
        found = true;
108
      }
109
      else {
110
        for( final TreeItem<T> child : node.getChildren() ) {
111
          stack.push( (VariableTreeItem<T>)child );
112
        }
113
114
        // No match found, yet.
115
        node = null;
116
      }
117
    }
118
119
    return (VariableTreeItem<T>)node;
120
  }
121
122
  /**
123
   * Returns true if this node is a leaf and its value starts with the given
124
   * text.
125
   *
126
   * @param s The text to compare against the node value.
127
   *
128
   * @return true Node is a leaf and its value starts with the given value.
129
   */
130
  private boolean valueStartsWith( final String s ) {
131
    return isLeaf() && getValue().toString().startsWith( s );
132
  }
133
134
  /**
135
   * Returns true if this node is a leaf and its value contains the given text.
136
   *
137
   * @param s The text to compare against the node value.
138
   *
139
   * @return true Node is a leaf and its value contains the given value.
140
   */
141
  private boolean valueContains( final String s ) {
142
    return isLeaf() && getValue().toString().contains( s );
143
  }
144
145
  /**
146
   * Returns the path for this node, with nodes made distinct using the
147
   * separator character. This uses two loops: one for pushing nodes onto a
148
   * stack and one for popping them off to create the path in desired order.
149
   *
150
   * @return A non-null string, possibly empty.
151
   */
152
  public String toPath() {
153
    final Stack<TreeItem<T>> stack = new Stack<>();
154
    TreeItem<T> node = this;
155
156
    while( node.getParent() != null ) {
157
      stack.push( node );
158
      node = node.getParent();
159
    }
160
161
    final StringBuilder sb = new StringBuilder( DEFAULT_MAX_VAR_LENGTH );
162
163
    while( !stack.isEmpty() ) {
164
      node = stack.pop();
165
166
      if( !node.isLeaf() ) {
167
        sb.append( node.getValue() );
168
169
        // This will add a superfluous separator, but instead of peeking at
170
        // the stack all the time, the last separator will be removed outside
171
        // the loop (one operation executed once).
172
        sb.append( SEPARATOR );
173
      }
174
    }
175
176
    // Remove the trailing SEPARATOR.
177
    if( sb.length() > 0 ) {
178
      sb.setLength( sb.length() - 1 );
179
    }
180
181
    return sb.toString();
182
  }
183
184
  /**
185
   * Returns the hierarchy, flattened to key-value pairs.
186
   *
187
   * @return A map of this tree's key-value pairs.
188
   */
189
  public Map<String, String> getMap() {
190
    if( this.map == null ) {
191
      this.map = new HashMap<>( DEFAULT_MAP_SIZE );
192
      populate( this, this.map );
193
    }
194
195
    return this.map;
196
  }
197
198
  private void populate( final TreeItem<T> parent, final Map<String, String> map ) {
199
    for( final TreeItem<T> child : parent.getChildren() ) {
200
      if( child.isLeaf() ) {
201
        @SuppressWarnings( "unchecked" )
202
        final String key = toVariable( ((VariableTreeItem<String>)child).toPath() );
203
        final String value = child.getValue().toString();
204
205
        map.put( key, value );
206
      }
207
      else {
208
        populate( child, map );
209
      }
210
    }
211
  }
212
213
  /**
214
   * Converts the name of the key to a simple variable by enclosing it with
215
   * dollar symbols.
216
   *
217
   * @param key The key name to change to a variable.
218
   *
219
   * @return $key$
220
   */
221
  public String toVariable( final String key ) {
222
    return VARIABLE_DECORATOR.decorate( key );
223
  }
224
}
1225
A src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import static com.scrivenvar.Messages.get;
31
import com.scrivenvar.definition.FileDefinitionSource;
32
import java.io.InputStream;
33
import java.nio.file.Files;
34
import java.nio.file.Path;
35
import java.util.Map;
36
import javafx.scene.control.TreeView;
37
38
/**
39
 * Represents a definition data source for YAML files.
40
 *
41
 * @author White Magic Software, Ltd.
42
 */
43
public class YamlFileDefinitionSource extends FileDefinitionSource {
44
45
  private YamlTreeAdapter yamlTreeAdapter;
46
  private YamlParser yamlParser;
47
48
  /**
49
   * Constructs a new YAML definition source, populated from the given file.
50
   *
51
   * @param path Path to the YAML definition file.
52
   */
53
  public YamlFileDefinitionSource( final Path path ) {
54
    super( path );
55
    init();
56
  }
57
  
58
  private void init() {
59
    setYamlParser( createYamlParser() );
60
  }
61
62
  @Override
63
  public Map<String, String> getResolvedMap() {
64
    return getYamlParser().createResolvedMap();
65
  }
66
67
  private YamlTreeAdapter getYamlTreeAdapter() {
68
    if( this.yamlTreeAdapter == null ) {
69
      setYamlTreeAdapter( new YamlTreeAdapter( getYamlParser() ) );
70
    }
71
72
    return this.yamlTreeAdapter;
73
  }
74
75
  private void setYamlTreeAdapter( final YamlTreeAdapter yamlTreeAdapter ) {
76
    this.yamlTreeAdapter = yamlTreeAdapter;
77
  }
78
79
  private YamlParser getYamlParser() {
80
    if( this.yamlParser == null ) {
81
      setYamlParser( createYamlParser() );
82
    }
83
84
    return this.yamlParser;
85
  }
86
87
  private void setYamlParser( final YamlParser yamlParser ) {
88
    this.yamlParser = yamlParser;
89
  }
90
91
  private YamlParser createYamlParser() {
92
    try( final InputStream in = Files.newInputStream( getPath() ) ) {
93
      return new YamlParser( in );
94
    } catch( final Exception ex ) {
95
      throw new RuntimeException( ex );
96
    }
97
  }
98
99
  @Override
100
  protected TreeView<String> createTreeView() {
101
    return getYamlTreeAdapter().adapt(
102
      get( "Pane.defintion.node.root.title" )
103
    );
104
  }
105
}
1106
A src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import com.fasterxml.jackson.core.JsonGenerationException;
31
import com.fasterxml.jackson.core.ObjectCodec;
32
import com.fasterxml.jackson.core.io.IOContext;
33
import com.fasterxml.jackson.databind.JsonNode;
34
import com.fasterxml.jackson.databind.ObjectMapper;
35
import com.fasterxml.jackson.databind.node.ObjectNode;
36
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
37
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
38
import com.scrivenvar.decorators.VariableDecorator;
39
import com.scrivenvar.decorators.YamlVariableDecorator;
40
import java.io.IOException;
41
import java.io.InputStream;
42
import java.io.Writer;
43
import java.text.MessageFormat;
44
import java.util.HashMap;
45
import java.util.Map;
46
import java.util.Map.Entry;
47
import java.util.regex.Matcher;
48
import java.util.regex.Pattern;
49
import org.yaml.snakeyaml.DumperOptions;
50
51
/**
52
 * <p>
53
 * This program loads a YAML document into memory, scans for variable
54
 * declarations, then substitutes any self-referential values back into the
55
 * document. Its output is the given YAML document without any variables.
56
 * Variables in the YAML document are denoted using a bracketed dollar symbol
57
 * syntax. For example: $field.name$. Some nomenclature to keep from going
58
 * squirrely, consider:
59
 * </p>
60
 *
61
 * <pre>
62
 *   root:
63
 *     node:
64
 *       name: $field.name$
65
 *   field:
66
 *     name: Alan Turing
67
 * </pre>
68
 *
69
 * The various components of the given YAML are called:
70
 *
71
 * <ul>
72
 * <li><code>$field.name$</code> - delimited reference</li>
73
 * <li><code>field.name</code> - reference</li>
74
 * <li><code>name</code> - YAML field</li>
75
 * <li><code>Alan Turing</code> - (dereferenced) field value</li>
76
 * </ul>
77
 *
78
 * @author White Magic Software, Ltd.
79
 */
80
public class YamlParser {
81
82
  /**
83
   * Separates YAML variable nodes (e.g., the dots in
84
   * <code>$root.node.var$</code>).
85
   */
86
  public static final String SEPARATOR = ".";
87
  public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 );
88
89
  private final static int GROUP_DELIMITED = 1;
90
  private final static int GROUP_REFERENCE = 2;
91
92
  private final static VariableDecorator VARIABLE_DECORATOR
93
    = new YamlVariableDecorator();
94
95
  private String error;
96
97
  /**
98
   * Compiled version of DEFAULT_REGEX.
99
   */
100
  private final static Pattern REGEX_PATTERN
101
    = Pattern.compile( YamlVariableDecorator.REGEX );
102
103
  /**
104
   * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values.
105
   */
106
  private final static char SEPARATOR_YAML = '/';
107
108
  /**
109
   * Start of the Universe (the YAML document node that contains all others).
110
   */
111
  private JsonNode documentRoot;
112
113
  /**
114
   * Map of references to dereferenced field values.
115
   */
116
  private Map<String, String> references;
117
118
  public YamlParser( final InputStream in ) throws IOException {
119
    process( in );
120
  }
121
122
  /**
123
   * Returns the given string with all the delimited references swapped with
124
   * their recursively resolved values.
125
   *
126
   * @param text The text to parse with zero or more delimited references to
127
   * replace.
128
   *
129
   * @return The substituted value.
130
   */
131
  public String substitute( String text ) {
132
    final Matcher matcher = patternMatch( text );
133
    final Map<String, String> map = getReferences();
134
135
    while( matcher.find() ) {
136
      final String key = matcher.group( GROUP_DELIMITED );
137
      final String value = map.get( key );
138
139
      if( value == null ) {
140
        missing( text );
141
      }
142
      else {
143
        text = text.replace( key, value );
144
      }
145
    }
146
147
    return text;
148
  }
149
150
  /**
151
   * Returns all the strings with their values resolved in a flat hierarchy.
152
   * This copies all the keys and resolved values into a new map.
153
   *
154
   * @return The new map created with all values having been resolved,
155
   * recursively.
156
   */
157
  public Map<String, String> createResolvedMap() {
158
    final Map<String, String> map = new HashMap<>( 1024 );
159
160
    resolve( getDocumentRoot(), "", map );
161
162
    return map;
163
  }
164
165
  /**
166
   * Iterate over a given root node (at any level of the tree) and adapt each
167
   * leaf node.
168
   *
169
   * @param rootNode A JSON node (YAML node) to adapt.
170
   * @param map Container that associates definitions with values.
171
   */
172
  private void resolve(
173
    final JsonNode rootNode,
174
    final String path,
175
    final Map<String, String> map ) {
176
177
    if( rootNode != null ) {
178
      rootNode.fields().forEachRemaining(
179
        (Entry<String, JsonNode> leaf) -> resolve( leaf, path, map )
180
      );
181
    }
182
  }
183
184
  /**
185
   * Recursively adapt each rootNode to a corresponding rootItem.
186
   *
187
   * @param rootNode The node to adapt.
188
   */
189
  private void resolve(
190
    final Entry<String, JsonNode> rootNode,
191
    final String path,
192
    final Map<String, String> map ) {
193
194
    final JsonNode leafNode = rootNode.getValue();
195
    final String key = rootNode.getKey();
196
197
    if( leafNode.isValueNode() ) {
198
      final String value = rootNode.getValue().asText();
199
200
      map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) );
201
    }
202
203
    if( leafNode.isObject() ) {
204
      resolve( leafNode, path + key + SEPARATOR, map );
205
    }
206
  }
207
208
  /**
209
   * Reads the first document from the given stream of YAML data and returns a
210
   * corresponding object that represents the YAML hierarchy. The calling class
211
   * is responsible for closing the stream. Calling classes should use
212
   * <code>JsonNode.fields()</code> to walk through the YAML tree of fields.
213
   *
214
   * @param in The input stream containing YAML content.
215
   *
216
   * @return An object hierarchy to represent the content.
217
   *
218
   * @throws IOException Could not read the stream.
219
   */
220
  private JsonNode process( final InputStream in ) throws IOException {
221
    final ObjectNode root = (ObjectNode)getObjectMapper().readTree( in );
222
    setDocumentRoot( root );
223
    process( root );
224
    return getDocumentRoot();
225
  }
226
227
  /**
228
   * Iterate over a given root node (at any level of the tree) and process each
229
   * leaf node.
230
   *
231
   * @param root A node to process.
232
   */
233
  private void process( final JsonNode root ) {
234
    root.fields().forEachRemaining( this::process );
235
  }
236
237
  /**
238
   * Process the given field, which is a named node. This is where the
239
   * application does the up-front work of mapping references to their fully
240
   * recursively dereferenced values.
241
   *
242
   * @param field The named node.
243
   */
244
  private void process( final Entry<String, JsonNode> field ) {
245
    final JsonNode node = field.getValue();
246
247
    if( node.isObject() ) {
248
      process( node );
249
    }
250
    else {
251
      final JsonNode fieldValue = field.getValue();
252
253
      // Only basic data types can be parsed into variable values. For
254
      // node structures, YAML has a built-in mechanism.
255
      if( fieldValue.isValueNode() ) {
256
        try {
257
          resolve( fieldValue.asText() );
258
        } catch( StackOverflowError e ) {
259
          setError( "Unresolvable: " + node.textValue() + " = " + fieldValue );
260
        }
261
      }
262
    }
263
  }
264
265
  /**
266
   * Inserts the delimited references and field values into the cache. This will
267
   * overwrite existing references.
268
   *
269
   * @param fieldValue YAML field containing zero or more delimited references.
270
   * If it contains a delimited reference, the parameter is modified with the
271
   * dereferenced value before it is returned.
272
   *
273
   * @return fieldValue without delimited references.
274
   */
275
  private String resolve( String fieldValue ) {
276
    final Matcher matcher = patternMatch( fieldValue );
277
278
    while( matcher.find() ) {
279
      final String delimited = matcher.group( GROUP_DELIMITED );
280
      final String reference = matcher.group( GROUP_REFERENCE );
281
      final String dereference = resolve( lookup( reference ) );
282
283
      fieldValue = fieldValue.replace( delimited, dereference );
284
285
      // This will perform some superfluous calls by overwriting existing
286
      // items in the delimited reference map.
287
      put( delimited, dereference );
288
    }
289
290
    return fieldValue;
291
  }
292
293
  /**
294
   * Inserts a key/value pair into the references map. The map retains
295
   * references and dereferenced values found in the YAML. If the reference
296
   * already exists, this will overwrite with a new value.
297
   *
298
   * @param delimited The variable name.
299
   * @param dereferenced The resolved value.
300
   */
301
  private void put( String delimited, String dereferenced ) {
302
    if( dereferenced.isEmpty() ) {
303
      missing( delimited );
304
    }
305
    else {
306
      getReferences().put( delimited, dereferenced );
307
    }
308
  }
309
310
  /**
311
   * Writes the modified YAML document to standard output.
312
   */
313
  private void writeDocument() throws IOException {
314
    getObjectMapper().writeValue( System.out, getDocumentRoot() );
315
  }
316
317
  /**
318
   * Called when a delimited reference is dereferenced to an empty string. This
319
   * should produce a warning for the user.
320
   *
321
   * @param delimited Delimited reference with no derived value.
322
   */
323
  private void missing( final String delimited ) {
324
    setError( MessageFormat.format( "Missing value for '{0}'.", delimited ) );
325
  }
326
327
  /**
328
   * Returns a REGEX_PATTERN matcher for the given text.
329
   *
330
   * @param text The text that contains zero or more instances of a
331
   * REGEX_PATTERN that can be found using the regular expression.
332
   */
333
  private Matcher patternMatch( String text ) {
334
    return getPattern().matcher( text );
335
  }
336
337
  /**
338
   * Finds the YAML value for a reference.
339
   *
340
   * @param reference References a value in the YAML document.
341
   *
342
   * @return The dereferenced value.
343
   */
344
  private String lookup( final String reference ) {
345
    return getDocumentRoot().at( asPath( reference ) ).asText();
346
  }
347
348
  /**
349
   * Converts a reference (not delimited) to a path that can be used to find a
350
   * value that should exist inside the YAML document.
351
   *
352
   * @param reference The reference to convert to a YAML document path.
353
   *
354
   * @return The reference with a leading slash and its separator characters
355
   * converted to slashes.
356
   */
357
  private String asPath( final String reference ) {
358
    return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(), SEPARATOR_YAML );
359
  }
360
361
  /**
362
   * Sets the parent node for the entire YAML document tree.
363
   *
364
   * @param documentRoot The parent node.
365
   */
366
  private void setDocumentRoot( ObjectNode documentRoot ) {
367
    this.documentRoot = documentRoot;
368
  }
369
370
  /**
371
   * Returns the parent node for the entire YAML document tree.
372
   *
373
   * @return The parent node.
374
   */
375
  protected JsonNode getDocumentRoot() {
376
    return this.documentRoot;
377
  }
378
379
  /**
380
   * Returns the compiled regular expression REGEX_PATTERN used to match
381
   * delimited references.
382
   *
383
   * @return A compiled regex for use with the Matcher.
384
   */
385
  private Pattern getPattern() {
386
    return REGEX_PATTERN;
387
  }
388
389
  /**
390
   * Returns the list of references mapped to dereferenced values.
391
   *
392
   * @return
393
   */
394
  private Map<String, String> getReferences() {
395
    if( this.references == null ) {
396
      this.references = createReferences();
397
    }
398
399
    return this.references;
400
  }
401
402
  /**
403
   * Subclasses can override this method to insert their own map.
404
   *
405
   * @return An empty HashMap, never null.
406
   */
407
  protected Map<String, String> createReferences() {
408
    return new HashMap<>();
409
  }
410
411
  private final class ResolverYAMLFactory extends YAMLFactory {
412
413
    private static final long serialVersionUID = 1L;
414
415
    @Override
416
    protected YAMLGenerator _createGenerator(
417
      final Writer out, final IOContext ctxt ) throws IOException {
418
419
      return new ResolverYAMLGenerator(
420
        ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
421
        out, _version );
422
    }
423
  }
424
425
  private class ResolverYAMLGenerator extends YAMLGenerator {
426
427
    public ResolverYAMLGenerator(
428
      final IOContext ctxt,
429
      final int jsonFeatures,
430
      final int yamlFeatures,
431
      final ObjectCodec codec,
432
      final Writer out,
433
      final DumperOptions.Version version ) throws IOException {
434
435
      super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
436
    }
437
438
    @Override
439
    public void writeString( final String text )
440
      throws IOException, JsonGenerationException {
441
      super.writeString( substitute( text ) );
442
    }
443
  }
444
445
  private YAMLFactory getYAMLFactory() {
446
    return new ResolverYAMLFactory();
447
  }
448
449
  private ObjectMapper getObjectMapper() {
450
    return new ObjectMapper( getYAMLFactory() );
451
  }
452
453
  /**
454
   * Returns the character used to separate YAML paths within delimited
455
   * references. This will return only the first character of the command line
456
   * parameter, if the default is overridden.
457
   *
458
   * @return A period by default.
459
   */
460
  private char getDelimitedSeparator() {
461
    return SEPARATOR.charAt( 0 );
462
  }
463
464
  private void setError( final String error ) {
465
    this.error = error;
466
  }
467
468
  /**
469
   * Returns the last error message, if any, that occurred during parsing.
470
   *
471
   * @return The error message or the empty string if no error occurred.
472
   */
473
  public String getError() {
474
    return this.error == null ? "" : this.error;
475
  }
476
}
1477
A src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.scrivenvar.definition.VariableTreeItem;
32
import java.util.Map.Entry;
33
import javafx.scene.control.TreeItem;
34
import javafx.scene.control.TreeView;
35
36
/**
37
 * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
38
 * interface.
39
 *
40
 * @author White Magic Software, Ltd.
41
 */
42
public class YamlTreeAdapter {
43
44
  private YamlParser yamlParser;
45
46
  public YamlTreeAdapter( final YamlParser parser ) {
47
    setYamlParser( parser );
48
  }
49
50
  /**
51
   * Converts a YAML document to a TreeView based on the document keys. Only the
52
   * first document in the stream is adapted.
53
   *
54
   * @param name Root TreeItem node name.
55
   *
56
   * @return A TreeView populated with all the keys in the YAML document.
57
   */
58
  public TreeView<String> adapt( final String name ){
59
    final JsonNode rootNode = getYamlParser().getDocumentRoot();
60
    final TreeItem<String> rootItem = createTreeItem( name );
61
62
    rootItem.setExpanded( true );
63
    adapt( rootNode, rootItem );
64
    return new TreeView<>( rootItem );
65
  }
66
67
  /**
68
   * Iterate over a given root node (at any level of the tree) and adapt each
69
   * leaf node.
70
   *
71
   * @param rootNode A JSON node (YAML node) to adapt.
72
   * @param rootItem The tree item to use as the root when processing the node.
73
   */
74
  private void adapt(
75
    final JsonNode rootNode, final TreeItem<String> rootItem ) {
76
77
    rootNode.fields().forEachRemaining(
78
      (Entry<String, JsonNode> leaf) -> adapt( leaf, rootItem )
79
    );
80
  }
81
82
  /**
83
   * Recursively adapt each rootNode to a corresponding rootItem.
84
   *
85
   * @param rootNode The node to adapt.
86
   * @param rootItem The item to adapt using the node's key.
87
   */
88
  private void adapt(
89
    final Entry<String, JsonNode> rootNode, final TreeItem<String> rootItem ) {
90
91
    final JsonNode leafNode = rootNode.getValue();
92
    final String key = rootNode.getKey();
93
    final TreeItem<String> leaf = createTreeItem( key );
94
95
    if( leafNode.isValueNode() ) {
96
      leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) );
97
    }
98
99
    rootItem.getChildren().add( leaf );
100
101
    if( leafNode.isObject() ) {
102
      adapt( leafNode, leaf );
103
    }
104
  }
105
106
  /**
107
   * Creates a new tree item that can be added to the tree view.
108
   *
109
   * @param value The node's value.
110
   *
111
   * @return A new tree item node, never null.
112
   */
113
  private TreeItem<String> createTreeItem( final String value ) {
114
    return new VariableTreeItem<>( value );
115
  }
116
117
  private YamlParser getYamlParser() {
118
    return this.yamlParser;
119
  }
120
121
  private void setYamlParser( final YamlParser yamlParser ) {
122
    this.yamlParser = yamlParser;
123
  }
124
}
1125
A src/main/java/com/scrivenvar/dialogs/AbstractDialog.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import static com.scrivenvar.Messages.get;
31
import com.scrivenvar.service.events.impl.ButtonOrderPane;
32
import static javafx.scene.control.ButtonType.CANCEL;
33
import static javafx.scene.control.ButtonType.OK;
34
import javafx.scene.control.Dialog;
35
import javafx.stage.Window;
36
37
/**
38
 * Superclass that abstracts common behaviours for all dialogs.
39
 *
40
 * @author White Magic Software, Ltd.
41
 * @param <T> The type of dialog to create (usually String).
42
 */
43
public abstract class AbstractDialog<T> extends Dialog<T> {
44
45
  /**
46
   * Ensures that all dialogs can be closed.
47
   *
48
   * @param owner The parent window of this dialog.
49
   * @param title The messages title to display in the title bar.
50
   */
51
  @SuppressWarnings( "OverridableMethodCallInConstructor" )
52
  public AbstractDialog( final Window owner, final String title ) {
53
    setTitle( get( title ) );
54
    setResizable( true );
55
56
    initOwner( owner );
57
    initCloseAction();
58
    initDialogPane();
59
    initDialogButtons();
60
    initComponents();
61
  }
62
63
  /**
64
   * Initialize the component layout.
65
   */
66
  protected abstract void initComponents();
67
68
  /**
69
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
70
   */
71
  protected void initDialogPane() {
72
    setDialogPane( new ButtonOrderPane() );
73
  }
74
  
75
  /**
76
   * Set an OK and CANCEL button on the dialog.
77
   */
78
  protected void initDialogButtons() {
79
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
80
  }
81
82
  /**
83
   * Attaches a setOnCloseRequest to the dialog's [X] button so that the user
84
   * can always close the window, even if there's an error.
85
   */
86
  protected final void initCloseAction() {
87
    final Window window = getDialogPane().getScene().getWindow();
88
    window.setOnCloseRequest( event -> window.hide() );
89
  }
90
}
191
A src/main/java/com/scrivenvar/dialogs/ImageDialog.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.dialogs;
28
29
import static com.scrivenvar.Messages.get;
30
import com.scrivenvar.controls.BrowseFileButton;
31
import com.scrivenvar.controls.EscapeTextField;
32
import java.nio.file.Path;
33
import javafx.application.Platform;
34
import javafx.beans.binding.Bindings;
35
import javafx.beans.property.SimpleStringProperty;
36
import javafx.beans.property.StringProperty;
37
import javafx.scene.control.ButtonBar.ButtonData;
38
import static javafx.scene.control.ButtonType.OK;
39
import javafx.scene.control.DialogPane;
40
import javafx.scene.control.Label;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
import javafx.stage.Window;
43
import org.tbee.javafx.scene.layout.fxml.MigPane;
44
45
/**
46
 * Dialog to enter a markdown image.
47
 *
48
 * @author Karl Tauber
49
 */
50
public class ImageDialog extends AbstractDialog<String> {
51
52
  private final StringProperty image = new SimpleStringProperty();
53
54
  public ImageDialog( final Window owner, final Path basePath ) {
55
    super(owner, "Dialog.image.title" );
56
    
57
    final DialogPane dialogPane = getDialogPane();
58
    dialogPane.setContent( pane );
59
60
    linkBrowseFileButton.setBasePath( basePath );
61
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
62
    linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
63
64
    dialogPane.lookupButton( OK ).disableProperty().bind(
65
      urlField.escapedTextProperty().isEmpty()
66
      .or( textField.escapedTextProperty().isEmpty() ) );
67
68
    image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
69
      .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
70
      .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
71
    previewField.textProperty().bind( image );
72
73
    setResultConverter( dialogButton -> {
74
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
75
      return (data == ButtonData.OK_DONE) ? image.get() : null;
76
    } );
77
78
    Platform.runLater( () -> {
79
      urlField.requestFocus();
80
81
      if( urlField.getText().startsWith( "http://" ) ) {
82
        urlField.selectRange( "http://".length(), urlField.getLength() );
83
      }
84
    } );
85
  }
86
87
  @Override
88
  protected void initComponents() {
89
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
90
    pane = new MigPane();
91
    Label urlLabel = new Label();
92
    urlField = new EscapeTextField();
93
    linkBrowseFileButton = new BrowseFileButton();
94
    Label textLabel = new Label();
95
    textField = new EscapeTextField();
96
    Label titleLabel = new Label();
97
    titleField = new EscapeTextField();
98
    Label previewLabel = new Label();
99
    previewField = new Label();
100
101
    //======== pane ========
102
    {
103
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
104
      pane.setRows( "[][][][]" );
105
106
      //---- urlLabel ----
107
      urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
108
      pane.add( urlLabel, "cell 0 0" );
109
110
      //---- urlField ----
111
      urlField.setEscapeCharacters( "()" );
112
      urlField.setText( "http://yourlink.com" );
113
      urlField.setPromptText( "http://yourlink.com" );
114
      pane.add( urlField, "cell 1 0" );
115
      pane.add( linkBrowseFileButton, "cell 2 0" );
116
117
      //---- textLabel ----
118
      textLabel.setText( get( "Dialog.image.textLabel.text" ) );
119
      pane.add( textLabel, "cell 0 1" );
120
121
      //---- textField ----
122
      textField.setEscapeCharacters( "[]" );
123
      pane.add( textField, "cell 1 1 2 1" );
124
125
      //---- titleLabel ----
126
      titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
127
      pane.add( titleLabel, "cell 0 2" );
128
      pane.add( titleField, "cell 1 2 2 1" );
129
130
      //---- previewLabel ----
131
      previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
132
      pane.add( previewLabel, "cell 0 3" );
133
      pane.add( previewField, "cell 1 3 2 1" );
134
    }
135
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
136
  }
137
138
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
139
  private MigPane pane;
140
  private EscapeTextField urlField;
141
  private BrowseFileButton linkBrowseFileButton;
142
  private EscapeTextField textField;
143
  private EscapeTextField titleField;
144
  private Label previewField;
145
	// JFormDesigner - End of variables declaration  //GEN-END:variables
146
}
1147
A src/main/java/com/scrivenvar/dialogs/ImageDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "ImageDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "ImageDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
34
				name: "linkBrowseFileButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "javafx.scene.control.Label" ) {
39
				name: "textLabel"
40
				"text": new FormMessage( null, "ImageDialog.textLabel.text" )
41
				auxiliary() {
42
					"JavaCodeGenerator.variableLocal": true
43
				}
44
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
45
				"value": "cell 0 1"
46
			} )
47
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
48
				name: "textField"
49
				"escapeCharacters": "[]"
50
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
51
				"value": "cell 1 1 2 1"
52
			} )
53
			add( new FormComponent( "javafx.scene.control.Label" ) {
54
				name: "titleLabel"
55
				"text": new FormMessage( null, "ImageDialog.titleLabel.text" )
56
				auxiliary() {
57
					"JavaCodeGenerator.variableLocal": true
58
				}
59
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
60
				"value": "cell 0 2"
61
			} )
62
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
63
				name: "titleField"
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 1 2 2 1"
66
			} )
67
			add( new FormComponent( "javafx.scene.control.Label" ) {
68
				name: "previewLabel"
69
				"text": new FormMessage( null, "ImageDialog.previewLabel.text" )
70
				auxiliary() {
71
					"JavaCodeGenerator.variableLocal": true
72
				}
73
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
74
				"value": "cell 0 3"
75
			} )
76
			add( new FormComponent( "javafx.scene.control.Label" ) {
77
				name: "previewField"
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 1 3 2 1"
80
			} )
81
		}, new FormLayoutConstraints( null ) {
82
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
83
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
84
		} )
85
	}
86
}
187
A src/main/java/com/scrivenvar/dialogs/LinkDialog.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import static com.scrivenvar.Messages.get;
31
import com.scrivenvar.controls.EscapeTextField;
32
import com.scrivenvar.editors.markdown.HyperlinkModel;
33
import java.nio.file.Path;
34
import javafx.application.Platform;
35
import javafx.beans.binding.Bindings;
36
import javafx.beans.property.SimpleStringProperty;
37
import javafx.beans.property.StringProperty;
38
import javafx.scene.control.ButtonBar.ButtonData;
39
import static javafx.scene.control.ButtonType.OK;
40
import javafx.scene.control.DialogPane;
41
import javafx.scene.control.Label;
42
import javafx.stage.Window;
43
import org.tbee.javafx.scene.layout.fxml.MigPane;
44
45
/**
46
 * Dialog to enter a markdown link.
47
 *
48
 * @author Karl Tauber
49
 */
50
public class LinkDialog extends AbstractDialog<String> {
51
52
  private final StringProperty link = new SimpleStringProperty();
53
54
  public LinkDialog(
55
    final Window owner, final HyperlinkModel hyperlink, final Path basePath ) {
56
    super( owner, "Dialog.link.title" );
57
58
    final DialogPane dialogPane = getDialogPane();
59
    dialogPane.setContent( pane );
60
61
    dialogPane.lookupButton( OK ).disableProperty().bind(
62
      urlField.escapedTextProperty().isEmpty() );
63
64
    textField.setText( hyperlink.getText() );
65
    urlField.setText( hyperlink.getUrl() );
66
    titleField.setText( hyperlink.getTitle() );
67
68
    link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
69
      .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
70
      .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
71
        .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
72
        .otherwise( urlField.escapedTextProperty() ) ) );
73
74
    setResultConverter( dialogButton -> {
75
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
76
      return (data == ButtonData.OK_DONE) ? link.get() : null;
77
    } );
78
79
    Platform.runLater( () -> {
80
      urlField.requestFocus();
81
      urlField.selectRange( 0, urlField.getLength() );
82
    } );
83
  }
84
85
  @Override
86
  protected void initComponents() {
87
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
88
    pane = new MigPane();
89
    Label urlLabel = new Label();
90
    urlField = new EscapeTextField();
91
    Label textLabel = new Label();
92
    textField = new EscapeTextField();
93
    Label titleLabel = new Label();
94
    titleField = new EscapeTextField();
95
96
    //======== pane ========
97
    {
98
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
99
      pane.setRows( "[][][][]" );
100
101
      //---- urlLabel ----
102
      urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
103
      pane.add( urlLabel, "cell 0 0" );
104
105
      //---- urlField ----
106
      urlField.setEscapeCharacters( "()" );
107
      pane.add( urlField, "cell 1 0" );
108
109
      //---- textLabel ----
110
      textLabel.setText( get( "Dialog.link.textLabel.text" ) );
111
      pane.add( textLabel, "cell 0 1" );
112
113
      //---- textField ----
114
      textField.setEscapeCharacters( "[]" );
115
      pane.add( textField, "cell 1 1 3 1" );
116
117
      //---- titleLabel ----
118
      titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
119
      pane.add( titleLabel, "cell 0 2" );
120
      pane.add( titleField, "cell 1 2 3 1" );
121
    }
122
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
123
  }
124
125
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
126
  private MigPane pane;
127
  private EscapeTextField urlField;
128
  private EscapeTextField textField;
129
  private EscapeTextField titleField;
130
  // JFormDesigner - End of variables declaration  //GEN-END:variables
131
}
1132
A src/main/java/com/scrivenvar/dialogs/LinkDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "LinkDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "LinkDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) {
34
				name: "linkBrowseDirectoyButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
39
				name: "linkBrowseFileButton"
40
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
41
				"value": "cell 3 0"
42
			} )
43
			add( new FormComponent( "javafx.scene.control.Label" ) {
44
				name: "textLabel"
45
				"text": new FormMessage( null, "LinkDialog.textLabel.text" )
46
				auxiliary() {
47
					"JavaCodeGenerator.variableLocal": true
48
				}
49
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
50
				"value": "cell 0 1"
51
			} )
52
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
53
				name: "textField"
54
				"escapeCharacters": "[]"
55
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
56
				"value": "cell 1 1 3 1"
57
			} )
58
			add( new FormComponent( "javafx.scene.control.Label" ) {
59
				name: "titleLabel"
60
				"text": new FormMessage( null, "LinkDialog.titleLabel.text" )
61
				auxiliary() {
62
					"JavaCodeGenerator.variableLocal": true
63
				}
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 0 2"
66
			} )
67
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
68
				name: "titleField"
69
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
70
				"value": "cell 1 2 3 1"
71
			} )
72
			add( new FormComponent( "javafx.scene.control.Label" ) {
73
				name: "previewLabel"
74
				"text": new FormMessage( null, "LinkDialog.previewLabel.text" )
75
				auxiliary() {
76
					"JavaCodeGenerator.variableLocal": true
77
				}
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 0 3"
80
			} )
81
			add( new FormComponent( "javafx.scene.control.Label" ) {
82
				name: "previewField"
83
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
84
				"value": "cell 1 3 3 1"
85
			} )
86
		}, new FormLayoutConstraints( null ) {
87
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
88
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
89
		} )
90
	}
91
}
192
A src/main/java/com/scrivenvar/dialogs/RScriptDialog.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import static com.scrivenvar.Messages.get;
31
import javafx.application.Platform;
32
import javafx.geometry.Insets;
33
import static javafx.scene.control.ButtonType.OK;
34
import javafx.scene.control.DialogPane;
35
import javafx.scene.control.Label;
36
import javafx.scene.control.TextArea;
37
import javafx.scene.layout.GridPane;
38
import javafx.stage.Window;
39
40
/**
41
 * Responsible for managing the R startup script that is run when an R source
42
 * file is loaded.
43
 *
44
 * @author White Magic Software, Ltd.
45
 */
46
public class RScriptDialog extends AbstractDialog<String> {
47
48
  private TextArea scriptArea;
49
  private String originalText = "";
50
51
  public RScriptDialog(
52
    final Window parent, final String title, final String script ) {
53
    super( parent, title );
54
    setOriginalText( script );
55
    getScriptArea().setText( script );
56
  }
57
58
  @Override
59
  protected void initComponents() {
60
    final DialogPane pane = getDialogPane();
61
62
    final GridPane grid = new GridPane();
63
    grid.setHgap( 10 );
64
    grid.setVgap( 10 );
65
    grid.setPadding( new Insets( 10, 10, 10, 10 ) );
66
67
    final Label label = new Label( get( "Dialog.rScript.content" ) );
68
69
    final TextArea textArea = getScriptArea();
70
    textArea.setEditable( true );
71
    textArea.setWrapText( true );
72
73
    grid.add( label, 0, 0 );
74
    grid.add( textArea, 0, 1 );
75
    pane.setContent( grid );
76
77
    Platform.runLater( () -> textArea.requestFocus() );
78
79
    setResultConverter( dialogButton -> {
80
      return dialogButton == OK ? textArea.getText() : getOriginalText();
81
    } );
82
  }
83
84
  private TextArea getScriptArea() {
85
    if( this.scriptArea == null ) {
86
      this.scriptArea = new TextArea();
87
    }
88
89
    return this.scriptArea;
90
  }
91
92
  private String getOriginalText() {
93
    return this.originalText;
94
  }
95
96
  private void setOriginalText( final String originalText ) {
97
    this.originalText = originalText;
98
  }
99
}
1100
A src/main/java/com/scrivenvar/editors/EditorPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.AbstractPane;
31
import java.nio.file.Path;
32
import java.util.function.Consumer;
33
import javafx.application.Platform;
34
import javafx.beans.property.ObjectProperty;
35
import javafx.beans.property.SimpleObjectProperty;
36
import javafx.beans.value.ChangeListener;
37
import javafx.event.Event;
38
import javafx.scene.control.ScrollPane;
39
import javafx.scene.input.InputEvent;
40
import org.fxmisc.flowless.VirtualizedScrollPane;
41
import org.fxmisc.richtext.StyleClassedTextArea;
42
import org.fxmisc.undo.UndoManager;
43
import org.fxmisc.wellbehaved.event.EventPattern;
44
import org.fxmisc.wellbehaved.event.InputMap;
45
import static org.fxmisc.wellbehaved.event.InputMap.consume;
46
import org.fxmisc.wellbehaved.event.Nodes;
47
48
/**
49
 * Represents common editing features for various types of text editors.
50
 *
51
 * @author White Magic Software, Ltd.
52
 */
53
public class EditorPane extends AbstractPane {
54
55
  private StyleClassedTextArea editor;
56
  private VirtualizedScrollPane<StyleClassedTextArea> scrollPane;
57
  private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
58
59
  /**
60
   * Set when entering variable edit mode; retrieved upon exiting.
61
   */
62
  private InputMap<InputEvent> nodeMap;
63
64
  @Override
65
  public void requestFocus() {
66
    Platform.runLater( () -> getEditor().requestFocus() );
67
  }
68
69
  public void undo() {
70
    getUndoManager().undo();
71
  }
72
73
  public void redo() {
74
    getUndoManager().redo();
75
  }
76
77
  /**
78
   * TOD: Implement this.
79
   */
80
  public void replace() {
81
  }
82
83
  /**
84
   * TOD: Implement this.
85
   */
86
  public void findPrevious() {
87
  }
88
89
  public UndoManager getUndoManager() {
90
    return getEditor().getUndoManager();
91
  }
92
93
  public String getText() {
94
    return getEditor().getText();
95
  }
96
97
  public void setText( final String text ) {
98
    getEditor().deselect();
99
    getEditor().replaceText( text );
100
    getUndoManager().mark();
101
  }
102
103
  /**
104
   * Call to hook into changes to the text area.
105
   *
106
   * @param listener Receives editor text change events.
107
   */
108
  public void addTextChangeListener( final ChangeListener<? super String> listener ) {
109
    getEditor().textProperty().addListener( listener );
110
  }
111
112
  /**
113
   * Call to listen for when the caret moves to another paragraph.
114
   *
115
   * @param listener Receives paragraph change events.
116
   */
117
  public void addCaretParagraphListener(
118
    final ChangeListener<? super Integer> listener ) {
119
    getEditor().currentParagraphProperty().addListener( listener );
120
  }
121
122
  /**
123
   * This method adds listeners to editor events.
124
   *
125
   * @param <T> The event type.
126
   * @param <U> The consumer type for the given event type.
127
   * @param event The event of interest.
128
   * @param consumer The method to call when the event happens.
129
   */
130
  public <T extends Event, U extends T> void addEventListener(
131
    final EventPattern<? super T, ? extends U> event,
132
    final Consumer<? super U> consumer ) {
133
    Nodes.addInputMap( getEditor(), consume( event, consumer ) );
134
  }
135
136
  /**
137
   * This method adds listeners to editor events that can be removed without
138
   * affecting the original listeners (i.e., the original lister is restored on
139
   * a call to removeEventListener).
140
   *
141
   * @param map The map of methods to events.
142
   */
143
  @SuppressWarnings( "unchecked" )
144
  public void addEventListener( final InputMap<InputEvent> map ) {
145
    this.nodeMap = (InputMap<InputEvent>)getInputMap();
146
    Nodes.addInputMap( getEditor(), map );
147
  }
148
149
  /**
150
   * This method removes listeners to editor events and restores the default
151
   * handler.
152
   *
153
   * @param map The map of methods to events.
154
   */
155
  public void removeEventListener( final InputMap<InputEvent> map ) {
156
    Nodes.removeInputMap( getEditor(), map );
157
    Nodes.addInputMap( getEditor(), this.nodeMap );
158
  }
159
160
  /**
161
   * Returns the value for "org.fxmisc.wellbehaved.event.inputmap".
162
   *
163
   * @return An input map of input events.
164
   */
165
  private Object getInputMap() {
166
    return getEditor().getProperties().get( getInputMapKey() );
167
  }
168
169
  /**
170
   * Returns the hashmap key entry for the input map.
171
   *
172
   * @return "org.fxmisc.wellbehaved.event.inputmap"
173
   */
174
  private String getInputMapKey() {
175
    return "org.fxmisc.wellbehaved.event.inputmap";
176
  }
177
178
  public void scrollToTop() {
179
    getEditor().moveTo( 0 );
180
  }
181
182
  private void setEditor( StyleClassedTextArea textArea ) {
183
    this.editor = textArea;
184
  }
185
186
  public synchronized StyleClassedTextArea getEditor() {
187
    if( this.editor == null ) {
188
      setEditor( createTextArea() );
189
    }
190
191
    return this.editor;
192
  }
193
194
  /**
195
   * Returns the scroll pane that contains the text area.
196
   *
197
   * @return The scroll pane that contains the content to edit.
198
   */
199
  public synchronized VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
200
    if( this.scrollPane == null ) {
201
      this.scrollPane = createScrollPane();
202
    }
203
204
    return this.scrollPane;
205
  }
206
207
  protected VirtualizedScrollPane<StyleClassedTextArea> createScrollPane() {
208
    final VirtualizedScrollPane<StyleClassedTextArea> pane
209
      = new VirtualizedScrollPane<>( getEditor() );
210
    pane.setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
211
212
    return pane;
213
  }
214
215
  protected StyleClassedTextArea createTextArea() {
216
    return new StyleClassedTextArea( false );
217
  }
218
219
  public Path getPath() {
220
    return this.path.get();
221
  }
222
223
  public void setPath( final Path path ) {
224
    this.path.set( path );
225
  }
226
227
  public ObjectProperty<Path> pathProperty() {
228
    return this.path;
229
  }
230
}
1231
A src/main/java/com/scrivenvar/editors/VariableNameDecoratorFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.decorators.RVariableDecorator;
32
import com.scrivenvar.decorators.VariableDecorator;
33
import com.scrivenvar.decorators.YamlVariableDecorator;
34
import java.nio.file.Path;
35
36
/**
37
 * Responsible for creating a variable name decorator suited to a particular
38
 * file type.
39
 *
40
 * @author White Magic Software, Ltd.
41
 */
42
public class VariableNameDecoratorFactory extends AbstractFileFactory {
43
44
  private VariableNameDecoratorFactory() {
45
  }
46
47
  public static VariableDecorator newInstance( final Path path ) {
48
    final VariableNameDecoratorFactory f = new VariableNameDecoratorFactory();
49
    final VariableDecorator result;
50
51
    switch( f.lookup( path ) ) {
52
      case RMARKDOWN:
53
      case RXML:
54
        result = new RVariableDecorator();
55
        break;
56
57
      default:
58
        result = new YamlVariableDecorator();
59
        break;
60
    }
61
62
    return result;
63
  }
64
}
165
A src/main/java/com/scrivenvar/editors/VariableNameInjector.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.FileEditorTab;
31
import com.scrivenvar.Services;
32
import com.scrivenvar.decorators.VariableDecorator;
33
import com.scrivenvar.definition.DefinitionPane;
34
import com.scrivenvar.definition.VariableTreeItem;
35
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR;
36
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR_CHAR;
37
import com.scrivenvar.service.Settings;
38
import static com.scrivenvar.util.Lists.getFirst;
39
import static com.scrivenvar.util.Lists.getLast;
40
import static java.lang.Character.isSpaceChar;
41
import static java.lang.Character.isWhitespace;
42
import static java.lang.Math.min;
43
import java.nio.file.Path;
44
import java.util.function.Consumer;
45
import javafx.collections.ObservableList;
46
import javafx.event.Event;
47
import javafx.scene.control.IndexRange;
48
import javafx.scene.control.TreeItem;
49
import javafx.scene.input.InputEvent;
50
import javafx.scene.input.KeyCode;
51
import static javafx.scene.input.KeyCode.AT;
52
import static javafx.scene.input.KeyCode.DIGIT2;
53
import static javafx.scene.input.KeyCode.ENTER;
54
import static javafx.scene.input.KeyCode.MINUS;
55
import static javafx.scene.input.KeyCode.SPACE;
56
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
57
import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
58
import javafx.scene.input.KeyEvent;
59
import org.fxmisc.richtext.StyledTextArea;
60
import org.fxmisc.wellbehaved.event.EventPattern;
61
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
62
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
63
import org.fxmisc.wellbehaved.event.InputMap;
64
import static org.fxmisc.wellbehaved.event.InputMap.consume;
65
import static org.fxmisc.wellbehaved.event.InputMap.sequence;
66
67
/**
68
 * Provides the logic for injecting variable names within the editor.
69
 *
70
 * @author White Magic Software, Ltd.
71
 */
72
public class VariableNameInjector {
73
74
  public static final int DEFAULT_MAX_VAR_LENGTH = 64;
75
76
  private static final int NO_DIFFERENCE = -1;
77
78
  private final Settings settings = Services.load( Settings.class );
79
80
  /**
81
   * Used to capture keyboard events once the user presses @.
82
   */
83
  private InputMap<InputEvent> keyboardMap;
84
85
  private FileEditorTab tab;
86
  private DefinitionPane definitionPane;
87
88
  /**
89
   * Position of the variable in the text when in variable mode (0 by default).
90
   */
91
  private int initialCaretPosition;
92
93
  private VariableNameInjector() {
94
  }
95
96
  public static void listen( final FileEditorTab tab, final DefinitionPane pane ) {
97
    VariableNameInjector vni = new VariableNameInjector();
98
99
    vni.setFileEditorTab( tab );
100
    vni.setDefinitionPane( pane );
101
102
    vni.initKeyboardEventListeners();
103
  }
104
105
  /**
106
   * Traps keys for performing various short-cut tasks, such as @-mode variable
107
   * insertion and control+space for variable autocomplete.
108
   *
109
   * @ key is pressed, a new keyboard map is inserted in place of the current
110
   * map -- this class goes into "variable edit mode" (a.k.a. vMode).
111
   *
112
   * @see createKeyboardMap()
113
   */
114
  private void initKeyboardEventListeners() {
115
    addEventListener( keyPressed( SPACE, CONTROL_DOWN ), this::autocomplete );
116
117
    // @ key in Linux?
118
    addEventListener( keyPressed( DIGIT2, SHIFT_DOWN ), this::vMode );
119
    // @ key in Windows.
120
    addEventListener( keyPressed( AT ), this::vMode );
121
  }
122
123
  /**
124
   * The @ symbol is a short-cut to inserting a YAML variable reference.
125
   *
126
   * @param e Superfluous information about the key that was pressed.
127
   */
128
  private void vMode( KeyEvent e ) {
129
    setInitialCaretPosition();
130
    vModeStart();
131
    vModeAutocomplete();
132
  }
133
134
  /**
135
   * Receives key presses until the user completes the variable selection. This
136
   * allows the arrow keys to be used for selecting variables.
137
   *
138
   * @param e The key that was pressed.
139
   */
140
  private void vModeKeyPressed( KeyEvent e ) {
141
    final KeyCode keyCode = e.getCode();
142
143
    switch( keyCode ) {
144
      case BACK_SPACE:
145
        // Don't decorate the variable upon exiting vMode.
146
        vModeBackspace();
147
        break;
148
149
      case ESCAPE:
150
        // Don't decorate the variable upon exiting vMode.
151
        vModeStop();
152
        break;
153
154
      case ENTER:
155
      case PERIOD:
156
      case RIGHT:
157
      case END:
158
        // Stop at a leaf node, ENTER means accept.
159
        if( vModeConditionalComplete() && keyCode == ENTER ) {
160
          vModeStop();
161
162
          // Decorate the variable upon exiting vMode.
163
          decorateVariable();
164
        }
165
        break;
166
167
      case UP:
168
        cyclePathPrev();
169
        break;
170
171
      case DOWN:
172
        cyclePathNext();
173
        break;
174
175
      default:
176
        vModeFilterKeyPressed( e );
177
        break;
178
    }
179
180
    e.consume();
181
  }
182
183
  private void vModeBackspace() {
184
    deleteSelection();
185
186
    // Break out of variable mode by back spacing to the original position.
187
    if( getCurrentCaretPosition() > getInitialCaretPosition() ) {
188
      vModeAutocomplete();
189
    }
190
    else {
191
      vModeStop();
192
    }
193
  }
194
195
  /**
196
   * Updates the text with the path selected (or typed) by the user.
197
   */
198
  private void vModeAutocomplete() {
199
    final TreeItem<String> node = getCurrentNode();
200
201
    if( node != null && !node.isLeaf() ) {
202
      final String word = getLastPathWord();
203
      final String label = node.getValue();
204
      final int delta = difference( label, word );
205
      final String remainder = delta == NO_DIFFERENCE
206
        ? label
207
        : label.substring( delta );
208
209
      final StyledTextArea textArea = getEditor();
210
      final int posBegan = getCurrentCaretPosition();
211
      final int posEnded = posBegan + remainder.length();
212
213
      textArea.replaceSelection( remainder );
214
215
      if( posEnded - posBegan > 0 ) {
216
        textArea.selectRange( posEnded, posBegan );
217
      }
218
219
      expand( node );
220
    }
221
  }
222
223
  /**
224
   * Only variable name keys can pass through the filter. This is called when
225
   * the user presses a key.
226
   *
227
   * @param e The key that was pressed.
228
   */
229
  private void vModeFilterKeyPressed( final KeyEvent e ) {
230
    if( isVariableNameKey( e ) ) {
231
      typed( e.getText() );
232
    }
233
  }
234
235
  /**
236
   * Performs an autocomplete depending on whether the user has finished typing
237
   * in a word. If there is a selected range, then this will complete the most
238
   * recent word and jump to the next child.
239
   *
240
   * @return true The auto-completed node was a terminal node.
241
   */
242
  private boolean vModeConditionalComplete() {
243
    acceptPath();
244
245
    final TreeItem<String> node = getCurrentNode();
246
    final boolean terminal = isTerminal( node );
247
248
    if( !terminal ) {
249
      typed( SEPARATOR );
250
    }
251
252
    return terminal;
253
  }
254
255
  /**
256
   * Pressing control+space will find a node that matches the current word and
257
   * substitute the YAML variable reference. This is called when the user is not
258
   * editing in vMode.
259
   *
260
   * @param e Ignored -- it can only be Ctrl+Space.
261
   */
262
  private void autocomplete( final KeyEvent e ) {
263
    final String paragraph = getCaretParagraph();
264
    final int[] boundaries = getWordBoundaries( paragraph );
265
    final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
266
267
    VariableTreeItem<String> leaf = findLeaf( word );
268
269
    if( leaf == null ) {
270
      // If a leaf doesn't match using "starts with", then try using "contains".
271
      leaf = findLeaf( word, true );
272
    }
273
274
    if( leaf != null ) {
275
      replaceText( boundaries[ 0 ], boundaries[ 1 ], leaf.toPath() );
276
      decorateVariable();
277
      expand( leaf );
278
    }
279
  }
280
281
  /**
282
   * Called when autocomplete finishes on a valid leaf or when the user presses
283
   * Enter to finish manual autocomplete.
284
   */
285
  private void decorateVariable() {
286
    // A little bit of duplication...
287
    final String paragraph = getCaretParagraph();
288
    final int[] boundaries = getWordBoundaries( paragraph );
289
    final String old = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
290
291
    final String newVariable = getVariableDecorator().decorate( old );
292
293
    final int posEnded = getCurrentCaretPosition();
294
    final int posBegan = posEnded - old.length();
295
296
    getEditor().replaceText( posBegan, posEnded, newVariable );
297
  }
298
299
  /**
300
   * Updates the text at the given position within the current paragraph.
301
   *
302
   * @param posBegan The starting index in the paragraph text to replace.
303
   * @param posEnded The ending index in the paragraph text to replace.
304
   * @param text Overwrite the paragraph substring with this text.
305
   */
306
  private void replaceText(
307
    final int posBegan, final int posEnded, final String text ) {
308
    final int p = getCurrentParagraph();
309
310
    getEditor().replaceText( p, posBegan, p, posEnded, text );
311
  }
312
313
  /**
314
   * Returns the caret's current paragraph position.
315
   *
316
   * @return A number greater than or equal to 0.
317
   */
318
  private int getCurrentParagraph() {
319
    return getEditor().getCurrentParagraph();
320
  }
321
322
  /**
323
   * Returns current word boundary indexes into the current paragraph, including
324
   * punctuation.
325
   *
326
   * @param p The paragraph wherein to hunt word boundaries.
327
   * @param offset The offset into the paragraph to begin scanning left and
328
   * right.
329
   *
330
   * @return The starting and ending index of the word closest to the caret.
331
   */
332
  private int[] getWordBoundaries( final String p, final int offset ) {
333
    // Remove dashes, but retain hyphens. Retain same number of characters
334
    // to preserve relative indexes.
335
    final String paragraph = p.replace( "---", "   " ).replace( "--", "  " );
336
337
    return getWordAt( paragraph, offset );
338
  }
339
340
  /**
341
   * Helper method to get the word boundaries for the current paragraph.
342
   *
343
   * @param paragraph
344
   *
345
   * @return
346
   */
347
  private int[] getWordBoundaries( final String paragraph ) {
348
    return getWordBoundaries( paragraph, getCurrentCaretColumn() );
349
  }
350
351
  /**
352
   * Given an arbitrary offset into a string, this returns the word at that
353
   * index. The inputs and outputs include:
354
   *
355
   * <ul>
356
   * <li>surrounded by space: <code>hello | world!</code> ("");</li>
357
   * <li>end of word: <code>hello| world!</code> ("hello");</li>
358
   * <li>start of a word: <code>hello |world!</code> ("world!");</li>
359
   * <li>within a word: <code>hello wo|rld!</code> ("world!");</li>
360
   * <li>end of a paragraph: <code>hello world!|</code> ("world!");</li>
361
   * <li>start of a paragraph: <code>|hello world!</code> ("hello!"); or</li>
362
   * <li>after punctuation: <code>hello world!|</code> ("world!").</li>
363
   * </ul>
364
   *
365
   * @param p The string to scan for a word.
366
   * @param offset The offset within s to begin searching for the nearest word
367
   * boundary, must not be out of bounds of s.
368
   *
369
   * @return The word in s at the offset.
370
   *
371
   * @see getWordBegan( String, int )
372
   * @see getWordEnded( String, int )
373
   */
374
  private int[] getWordAt( final String p, final int offset ) {
375
    return new int[]{ getWordBegan( p, offset ), getWordEnded( p, offset ) };
376
  }
377
378
  /**
379
   * Returns the index into s where a word begins.
380
   *
381
   * @param s Never null.
382
   * @param offset Index into s to begin searching backwards for a word
383
   * boundary.
384
   *
385
   * @return The index where a word begins.
386
   */
387
  private int getWordBegan( final String s, int offset ) {
388
    while( offset > 0 && isBoundary( s.charAt( offset - 1 ) ) ) {
389
      offset--;
390
    }
391
392
    return offset;
393
  }
394
395
  /**
396
   * Returns the index into s where a word ends.
397
   *
398
   * @param s Never null.
399
   * @param offset Index into s to begin searching forwards for a word boundary.
400
   *
401
   * @return The index where a word ends.
402
   */
403
  private int getWordEnded( final String s, int offset ) {
404
    final int length = s.length();
405
406
    while( offset < length && isBoundary( s.charAt( offset ) ) ) {
407
      offset++;
408
    }
409
410
    return offset;
411
  }
412
413
  /**
414
   * Returns true if the given character can be reasonably expected to be part
415
   * of a word, including punctuation marks.
416
   *
417
   * @param c The character to compare.
418
   *
419
   * @return false The character is a space character.
420
   */
421
  private boolean isBoundary( final char c ) {
422
    return !isSpaceChar( c );
423
  }
424
425
  /**
426
   * Returns the text for the paragraph that contains the caret.
427
   *
428
   * @return A non-null string, possibly empty.
429
   */
430
  private String getCaretParagraph() {
431
    return getEditor().getText( getCurrentParagraph() );
432
  }
433
434
  /**
435
   * Returns true if the node has children that can be selected (i.e., any
436
   * non-leaves).
437
   *
438
   * @param <T> The type that the TreeItem contains.
439
   * @param node The node to test for terminality.
440
   *
441
   * @return true The node has one branch and its a leaf.
442
   */
443
  private <T> boolean isTerminal( final TreeItem<T> node ) {
444
    final ObservableList<TreeItem<T>> branches = node.getChildren();
445
446
    return branches.size() == 1 && branches.get( 0 ).isLeaf();
447
  }
448
449
  /**
450
   * Inserts text that the user typed at the current caret position, then
451
   * performs an autocomplete for the variable name.
452
   *
453
   * @param text The text to insert, never null.
454
   */
455
  private void typed( final String text ) {
456
    getEditor().replaceSelection( text );
457
    vModeAutocomplete();
458
  }
459
460
  /**
461
   * Called when the user presses either End or Enter key.
462
   */
463
  private void acceptPath() {
464
    final IndexRange range = getSelectionRange();
465
466
    if( range != null ) {
467
      final int rangeEnd = range.getEnd();
468
      final StyledTextArea textArea = getEditor();
469
      textArea.deselect();
470
      textArea.moveTo( rangeEnd );
471
    }
472
  }
473
474
  /**
475
   * Replaces the entirety of the existing path (from the initial caret
476
   * position) with the given path.
477
   *
478
   * @param oldPath The path to replace.
479
   * @param newPath The replacement path.
480
   */
481
  private void replacePath( final String oldPath, final String newPath ) {
482
    final StyledTextArea textArea = getEditor();
483
    final int posBegan = getInitialCaretPosition();
484
    final int posEnded = posBegan + oldPath.length();
485
486
    textArea.deselect();
487
    textArea.replaceText( posBegan, posEnded, newPath );
488
  }
489
490
  /**
491
   * Called when the user presses the Backspace key.
492
   */
493
  private void deleteSelection() {
494
    final StyledTextArea textArea = getEditor();
495
    textArea.replaceSelection( "" );
496
    textArea.deletePreviousChar();
497
  }
498
499
  /**
500
   * Cycles the selected text through the nodes.
501
   *
502
   * @param direction true - next; false - previous
503
   */
504
  private void cycleSelection( final boolean direction ) {
505
    final TreeItem<String> node = getCurrentNode();
506
507
    // Find the sibling for the current selection and replace the current
508
    // selection with the sibling's value
509
    TreeItem< String> cycled = direction
510
      ? node.nextSibling()
511
      : node.previousSibling();
512
513
    // When cycling at the end (or beginning) of the list, jump to the first
514
    // (or last) sibling depending on the cycle direction.
515
    if( cycled == null ) {
516
      cycled = direction ? getFirstSibling( node ) : getLastSibling( node );
517
    }
518
519
    final String path = getCurrentPath();
520
    final String cycledWord = cycled.getValue();
521
    final String word = getLastPathWord();
522
    final int index = path.indexOf( word );
523
    final String cycledPath = path.substring( 0, index ) + cycledWord;
524
525
    expand( cycled );
526
    replacePath( path, cycledPath );
527
  }
528
529
  /**
530
   * Cycles to the next sibling of the currently selected tree node.
531
   */
532
  private void cyclePathNext() {
533
    cycleSelection( true );
534
  }
535
536
  /**
537
   * Cycles to the previous sibling of the currently selected tree node.
538
   */
539
  private void cyclePathPrev() {
540
    cycleSelection( false );
541
  }
542
543
  /**
544
   * Returns the variable name (or as much as has been typed so far). Returns
545
   * all the characters from the initial caret column to the the first
546
   * whitespace character. This will return a path that contains zero or more
547
   * separators.
548
   *
549
   * @return A non-null string, possibly empty.
550
   */
551
  private String getCurrentPath() {
552
    final String s = extractTextChunk();
553
    final int length = s.length();
554
555
    int i = 0;
556
557
    while( i < length && !isWhitespace( s.charAt( i ) ) ) {
558
      i++;
559
    }
560
561
    return s.substring( 0, i );
562
  }
563
564
  private <T> ObservableList<TreeItem<T>> getSiblings(
565
    final TreeItem<T> item ) {
566
    final TreeItem<T> parent = item.getParent();
567
    return parent == null ? item.getChildren() : parent.getChildren();
568
  }
569
570
  private <T> TreeItem<T> getFirstSibling( final TreeItem<T> item ) {
571
    return getFirst( getSiblings( item ), item );
572
  }
573
574
  private <T> TreeItem<T> getLastSibling( final TreeItem<T> item ) {
575
    return getLast( getSiblings( item ), item );
576
  }
577
578
  /**
579
   * Returns the caret position as an offset into the text.
580
   *
581
   * @return A value from 0 to the length of the text (minus one).
582
   */
583
  private int getCurrentCaretPosition() {
584
    return getEditor().getCaretPosition();
585
  }
586
587
  /**
588
   * Returns the caret position within the current paragraph.
589
   *
590
   * @return A value from 0 to the length of the current paragraph.
591
   */
592
  private int getCurrentCaretColumn() {
593
    return getEditor().getCaretColumn();
594
  }
595
596
  /**
597
   * Returns the last word from the path.
598
   *
599
   * @return The last token.
600
   */
601
  private String getLastPathWord() {
602
    String path = getCurrentPath();
603
604
    int i = path.indexOf( SEPARATOR_CHAR );
605
606
    while( i > 0 ) {
607
      path = path.substring( i + 1 );
608
      i = path.indexOf( SEPARATOR_CHAR );
609
    }
610
611
    return path;
612
  }
613
614
  /**
615
   * Returns text from the initial caret position until some arbitrarily long
616
   * number of characters. The number of characters extracted will be
617
   * getMaxVarLength, or fewer, depending on how many characters remain to be
618
   * extracted. The result from this method is trimmed to the first whitespace
619
   * character.
620
   *
621
   * @return A chunk of text that includes all the words representing a path,
622
   * and then some.
623
   */
624
  private String extractTextChunk() {
625
    final StyledTextArea textArea = getEditor();
626
    final int textBegan = getInitialCaretPosition();
627
    final int remaining = textArea.getLength() - textBegan;
628
    final int textEnded = min( remaining, getMaxVarLength() );
629
630
    try {
631
      return textArea.getText( textBegan, textEnded );
632
    } catch( final Exception e ) {
633
      return textArea.getText();
634
    }
635
  }
636
637
  /**
638
   * Returns the node for the current path.
639
   */
640
  private TreeItem<String> getCurrentNode() {
641
    return findNode( getCurrentPath() );
642
  }
643
644
  /**
645
   * Finds the node that most closely matches the given path.
646
   *
647
   * @param path The path that represents a node.
648
   *
649
   * @return The node for the path, or the root node if the path could not be
650
   * found, but never null.
651
   */
652
  private TreeItem<String> findNode( final String path ) {
653
    return getDefinitionPane().findNode( path );
654
  }
655
656
  /**
657
   * Finds the first leaf having a value that starts with the given text.
658
   *
659
   * @param text The text to find in the definition tree.
660
   *
661
   * @return The leaf that starts with the given text, or null if not found.
662
   */
663
  private VariableTreeItem<String> findLeaf( final String text ) {
664
    return getDefinitionPane().findLeaf( text, false );
665
  }
666
667
  /**
668
   * Finds the first leaf having a value that starts with the given text, or
669
   * contains the text if contains is true.
670
   *
671
   * @param text The text to find in the definition tree.
672
   * @param contains Set true to perform a substring match after a starts with
673
   * match.
674
   *
675
   * @return The leaf that starts with the given text, or null if not found.
676
   */
677
  private VariableTreeItem<String> findLeaf(
678
    final String text,
679
    final boolean contains ) {
680
    return getDefinitionPane().findLeaf( text, contains );
681
  }
682
683
  /**
684
   * Used to ignore typed keys in favour of trapping pressed keys.
685
   *
686
   * @param e The key that was typed.
687
   */
688
  private void vModeKeyTyped( KeyEvent e ) {
689
    e.consume();
690
  }
691
692
  /**
693
   * Used to lazily initialize the keyboard map.
694
   *
695
   * @return Mappings for keyTyped and keyPressed.
696
   */
697
  protected InputMap<InputEvent> createKeyboardMap() {
698
    return sequence(
699
      consume( keyTyped(), this::vModeKeyTyped ),
700
      consume( keyPressed(), this::vModeKeyPressed )
701
    );
702
  }
703
704
  private InputMap<InputEvent> getKeyboardMap() {
705
    if( this.keyboardMap == null ) {
706
      this.keyboardMap = createKeyboardMap();
707
    }
708
709
    return this.keyboardMap;
710
  }
711
712
  /**
713
   * Collapses the tree then expands and selects the given node.
714
   *
715
   * @param node The node to expand.
716
   */
717
  private void expand( final TreeItem<String> node ) {
718
    final DefinitionPane pane = getDefinitionPane();
719
    pane.collapse();
720
    pane.expand( node );
721
    pane.select( node );
722
  }
723
724
  /**
725
   * Returns true iff the key code the user typed can be used as part of a YAML
726
   * variable name.
727
   *
728
   * @param keyEvent Keyboard key press event information.
729
   *
730
   * @return true The key is a value that can be inserted into the text.
731
   */
732
  private boolean isVariableNameKey( final KeyEvent keyEvent ) {
733
    final KeyCode kc = keyEvent.getCode();
734
735
    return (kc.isLetterKey()
736
      || kc.isDigitKey()
737
      || (keyEvent.isShiftDown() && kc == MINUS))
738
      && !keyEvent.isControlDown();
739
  }
740
741
  /**
742
   * Starts to capture user input events.
743
   */
744
  private void vModeStart() {
745
    addEventListener( getKeyboardMap() );
746
  }
747
748
  /**
749
   * Restores capturing of user input events to the previous event listener.
750
   * Also asks the processing chain to modify the variable text into a
751
   * machine-readable variable based on the format required by the file type.
752
   * For example, a Markdown file (.md) will substitute a $VAR$ name while an R
753
   * file (.Rmd, .Rxml) will use `r#xVAR`.
754
   */
755
  private void vModeStop() {
756
    removeEventListener( getKeyboardMap() );
757
  }
758
759
  /**
760
   * Returns a variable decorator that corresponds to the given file type.
761
   *
762
   * @return
763
   */
764
  private VariableDecorator getVariableDecorator() {
765
    return VariableNameDecoratorFactory.newInstance( getFilename() );
766
  }
767
768
  private Path getFilename() {
769
    return getFileEditorTab().getPath();
770
  }
771
772
  /**
773
   * Returns the index where the two strings diverge.
774
   *
775
   * @param s1 The string that could be a substring of s2, null allowed.
776
   * @param s2 The string that could be a substring of s1, null allowed.
777
   *
778
   * @return NO_DIFFERENCE if the strings are the same, otherwise the index
779
   * where they differ.
780
   */
781
  @SuppressWarnings( "StringEquality" )
782
  private int difference( final CharSequence s1, final CharSequence s2 ) {
783
    if( s1 == s2 ) {
784
      return NO_DIFFERENCE;
785
    }
786
787
    if( s1 == null || s2 == null ) {
788
      return 0;
789
    }
790
791
    int i = 0;
792
    final int limit = min( s1.length(), s2.length() );
793
794
    while( i < limit && s1.charAt( i ) == s2.charAt( i ) ) {
795
      i++;
796
    }
797
798
    // If one string was shorter than the other, that's where they differ.
799
    return i;
800
  }
801
802
  private EditorPane getEditorPane() {
803
    return getFileEditorTab().getEditorPane();
804
  }
805
806
  /**
807
   * Delegates to the file editor pane, and, ultimately, to its text area.
808
   */
809
  private <T extends Event, U extends T> void addEventListener(
810
    final EventPattern<? super T, ? extends U> event,
811
    final Consumer<? super U> consumer ) {
812
    getEditorPane().addEventListener( event, consumer );
813
  }
814
815
  /**
816
   * Delegates to the file editor pane, and, ultimately, to its text area.
817
   *
818
   * @param map The map of methods to events.
819
   */
820
  private void addEventListener( final InputMap<InputEvent> map ) {
821
    getEditorPane().addEventListener( map );
822
  }
823
824
  private void removeEventListener( final InputMap<InputEvent> map ) {
825
    getEditorPane().removeEventListener( map );
826
  }
827
828
  /**
829
   * Returns the position of the caret when variable mode editing was requested.
830
   *
831
   * @return The variable mode caret position.
832
   */
833
  private int getInitialCaretPosition() {
834
    return this.initialCaretPosition;
835
  }
836
837
  /**
838
   * Sets the position of the caret when variable mode editing was requested.
839
   * Stores the current position because only the text that comes afterwards is
840
   * a suitable variable reference.
841
   *
842
   * @return The variable mode caret position.
843
   */
844
  private void setInitialCaretPosition() {
845
    this.initialCaretPosition = getEditor().getCaretPosition();
846
  }
847
848
  private StyledTextArea getEditor() {
849
    return getFileEditorTab().getEditorPane().getEditor();
850
  }
851
852
  public FileEditorTab getFileEditorTab() {
853
    return this.tab;
854
  }
855
856
  private void setFileEditorTab( final FileEditorTab editorTab ) {
857
    this.tab = editorTab;
858
  }
859
860
  private DefinitionPane getDefinitionPane() {
861
    return this.definitionPane;
862
  }
863
864
  private void setDefinitionPane( final DefinitionPane definitionPane ) {
865
    this.definitionPane = definitionPane;
866
  }
867
868
  private IndexRange getSelectionRange() {
869
    return getEditor().getSelection();
870
  }
871
872
  /**
873
   * Don't look ahead too far when trying to find the end of a node.
874
   *
875
   * @return 512 by default.
876
   */
877
  private int getMaxVarLength() {
878
    return getSettings().getSetting(
879
      "editor.variable.maxLength", DEFAULT_MAX_VAR_LENGTH );
880
  }
881
882
  private Settings getSettings() {
883
    return this.settings;
884
  }
885
}
1886
A src/main/java/com/scrivenvar/editors/markdown/HyperlinkModel.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
32
/**
33
 * Represents the model for a hyperlink: text and url text.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public class HyperlinkModel {
38
39
  private String text;
40
  private String url;
41
  private String title;
42
43
  /**
44
   * Constructs a new hyperlink model in Markdown format by default with no
45
   * title (i.e., tooltip).
46
   *
47
   * @param text The hyperlink text displayed (e.g., displayed to the user).
48
   * @param url The destination URL (e.g., when clicked).
49
   */
50
  public HyperlinkModel( final String text, final String url ) {
51
    this( text, url, null );
52
  }
53
54
  /**
55
   * Constructs a new hyperlink model for the given AST link.
56
   * 
57
   * @param link A markdown link.
58
   */
59
  public HyperlinkModel( final Link link ) {
60
    this(
61
      link.getText().toString(),
62
      link.getUrl().toString(),
63
      link.getTitle().toString()
64
    );
65
  }
66
67
  /**
68
   * Constructs a new hyperlink model in Markdown format by default.
69
   *
70
   * @param text The hyperlink text displayed (e.g., displayed to the user).
71
   * @param url The destination URL (e.g., when clicked).
72
   * @param title The hyperlink title (e.g., shown as a tooltip).
73
   */
74
  public HyperlinkModel( final String text, final String url, final String title ) {
75
    setText( text );
76
    setUrl( url );
77
    setTitle( title );
78
  }
79
80
  /**
81
   * Returns the string in Markdown format by default.
82
   *
83
   * @return A markdown version of the hyperlink.
84
   */
85
  @Override
86
  public String toString() {
87
    String format = "%s%s%s";
88
89
    if( hasText() ) {
90
      format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
91
    }
92
93
    // Becomes ""+URL+"" if no text is set.
94
    // Becomes [TITLE]+(URL)+"" if no title is set.
95
    // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
96
    return String.format( format, getText(), getUrl(), getTitle() );
97
  }
98
99
  public final void setText( final String text ) {
100
    this.text = nullSafe( text );
101
  }
102
103
  public final void setUrl( final String url ) {
104
    this.url = nullSafe( url );
105
  }
106
107
  public final void setTitle( final String title ) {
108
    this.title = nullSafe( title );
109
  }
110
111
  /**
112
   * Answers whether text has been set for the hyperlink.
113
   *
114
   * @return true This is a text link.
115
   */
116
  public boolean hasText() {
117
    return !getText().isEmpty();
118
  }
119
120
  /**
121
   * Answers whether a title (tooltip) has been set for the hyperlink.
122
   *
123
   * @return true There is a title.
124
   */
125
  public boolean hasTitle() {
126
    return !getTitle().isEmpty();
127
  }
128
129
  public String getText() {
130
    return this.text;
131
  }
132
133
  public String getUrl() {
134
    return this.url;
135
  }
136
137
  public String getTitle() {
138
    return this.title;
139
  }
140
141
  private String nullSafe( final String s ) {
142
    return s == null ? "" : s;
143
  }
144
}
1145
A src/main/java/com/scrivenvar/editors/markdown/LinkVisitor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
import com.vladsch.flexmark.ast.Node;
32
import com.vladsch.flexmark.ast.NodeVisitor;
33
import com.vladsch.flexmark.ast.VisitHandler;
34
35
/**
36
 * @author White Magic Software, Ltd.
37
 */
38
public class LinkVisitor {
39
40
  private NodeVisitor visitor;
41
  private Link link;
42
  private final int offset;
43
44
  /**
45
   * Creates a hyperlink given an offset into a paragraph and the markdown AST
46
   * link node.
47
   *
48
   * @param index Index into the paragraph that indicates the hyperlink to
49
   * change.
50
   */
51
  public LinkVisitor( final int index ) {
52
    this.offset = index;
53
  }
54
55
  public Link process( final Node root ) {
56
    getVisitor().visit( root );
57
    return getLink();
58
  }
59
60
  /**
61
   *
62
   * @param link Not null.
63
   */
64
  private void visit( final Link link ) {
65
    final int began = link.getStartOffset();
66
    final int ended = link.getEndOffset();
67
    final int index = getOffset();
68
69
    if( index >= began && index <= ended ) {
70
      setLink( link );
71
    }
72
  }
73
74
  private synchronized NodeVisitor getVisitor() {
75
    if( this.visitor == null ) {
76
      this.visitor = createVisitor();
77
    }
78
79
    return this.visitor;
80
  }
81
82
  protected NodeVisitor createVisitor() {
83
    return new NodeVisitor(
84
      new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
85
  }
86
87
  private Link getLink() {
88
    return this.link;
89
  }
90
91
  private void setLink( final Link link ) {
92
    this.link = link;
93
  }
94
95
  public int getOffset() {
96
    return this.offset;
97
  }
98
}
199
A src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN;
31
import com.scrivenvar.dialogs.ImageDialog;
32
import com.scrivenvar.dialogs.LinkDialog;
33
import com.scrivenvar.editors.EditorPane;
34
import com.scrivenvar.processors.MarkdownProcessor;
35
import static com.scrivenvar.util.Utils.ltrim;
36
import static com.scrivenvar.util.Utils.rtrim;
37
import com.vladsch.flexmark.ast.Link;
38
import com.vladsch.flexmark.ast.Node;
39
import java.nio.file.Path;
40
import java.util.regex.Matcher;
41
import java.util.regex.Pattern;
42
import javafx.beans.value.ObservableValue;
43
import javafx.scene.control.Dialog;
44
import javafx.scene.control.IndexRange;
45
import static javafx.scene.input.KeyCode.ENTER;
46
import javafx.scene.input.KeyEvent;
47
import javafx.stage.Window;
48
import org.fxmisc.richtext.StyleClassedTextArea;
49
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
50
51
/**
52
 * Markdown editor pane.
53
 *
54
 * @author Karl Tauber and White Magic Software, Ltd.
55
 */
56
public class MarkdownEditorPane extends EditorPane {
57
58
  private static final Pattern AUTO_INDENT_PATTERN = Pattern.compile(
59
    "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
60
61
  public MarkdownEditorPane() {
62
    initEditor();
63
  }
64
65
  private void initEditor() {
66
    final StyleClassedTextArea textArea = getEditor();
67
68
    textArea.setWrapText( true );
69
    textArea.getStyleClass().add( "markdown-editor" );
70
    textArea.getStylesheets().add( STYLESHEET_MARKDOWN );
71
72
    addEventListener( keyPressed( ENTER ), this::enterPressed );
73
  }
74
75
  public ObservableValue<String> markdownProperty() {
76
    return getEditor().textProperty();
77
  }
78
79
  private void enterPressed( final KeyEvent e ) {
80
    final StyleClassedTextArea textArea = getEditor();
81
    final String currentLine = textArea.getText( textArea.getCurrentParagraph() );
82
    final Matcher matcher = AUTO_INDENT_PATTERN.matcher( currentLine );
83
84
    String newText = "\n";
85
86
    if( matcher.matches() ) {
87
      if( !matcher.group( 2 ).isEmpty() ) {
88
        // indent new line with same whitespace characters and list markers as current line
89
        newText = newText.concat( matcher.group( 1 ) );
90
      }
91
      else {
92
        // current line contains only whitespace characters and list markers
93
        // --> empty current line
94
        final int caretPosition = textArea.getCaretPosition();
95
        textArea.selectRange( caretPosition - currentLine.length(), caretPosition );
96
      }
97
    }
98
99
    textArea.replaceSelection( newText );
100
101
    // Ensure that the window scrolls when Enter is pressed at the bottom of
102
    // the pane.
103
    textArea.requestFollowCaret();
104
  }
105
106
  public void surroundSelection( final String leading, final String trailing ) {
107
    surroundSelection( leading, trailing, null );
108
  }
109
110
  public void surroundSelection( String leading, String trailing, final String hint ) {
111
    final StyleClassedTextArea textArea = getEditor();
112
113
    // Note: not using textArea.insertText() to insert leading and trailing
114
    // because this would add two changes to undo history
115
    final IndexRange selection = textArea.getSelection();
116
    int start = selection.getStart();
117
    int end = selection.getEnd();
118
119
    final String selectedText = textArea.getSelectedText();
120
121
    // remove leading and trailing whitespaces from selected text
122
    String trimmedSelectedText = selectedText.trim();
123
    if( trimmedSelectedText.length() < selectedText.length() ) {
124
      start += selectedText.indexOf( trimmedSelectedText );
125
      end = start + trimmedSelectedText.length();
126
    }
127
128
    // remove leading whitespaces from leading text if selection starts at zero
129
    if( start == 0 ) {
130
      leading = ltrim( leading );
131
    }
132
133
    // remove trailing whitespaces from trailing text if selection ends at text end
134
    if( end == textArea.getLength() ) {
135
      trailing = rtrim( trailing );
136
    }
137
138
    // remove leading line separators from leading text
139
    // if there are line separators before the selected text
140
    if( leading.startsWith( "\n" ) ) {
141
      for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
142
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
143
          break;
144
        }
145
        leading = leading.substring( 1 );
146
      }
147
    }
148
149
    // remove trailing line separators from trailing or leading text
150
    // if there are line separators after the selected text
151
    final boolean trailingIsEmpty = trailing.isEmpty();
152
    String str = trailingIsEmpty ? leading : trailing;
153
154
    if( str.endsWith( "\n" ) ) {
155
      final int length = textArea.getLength();
156
157
      for( int i = end; i < length && str.endsWith( "\n" ); i++ ) {
158
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
159
          break;
160
        }
161
162
        str = str.substring( 0, str.length() - 1 );
163
      }
164
165
      if( trailingIsEmpty ) {
166
        leading = str;
167
      }
168
      else {
169
        trailing = str;
170
      }
171
    }
172
173
    int selStart = start + leading.length();
174
    int selEnd = end + leading.length();
175
176
    // insert hint text if selection is empty
177
    if( hint != null && trimmedSelectedText.isEmpty() ) {
178
      trimmedSelectedText = hint;
179
      selEnd = selStart + hint.length();
180
    }
181
182
    // prevent undo merging with previous text entered by user
183
    getUndoManager().preventMerge();
184
185
    // replace text and update selection
186
    textArea.replaceText( start, end, leading + trimmedSelectedText + trailing );
187
    textArea.selectRange( selStart, selEnd );
188
  }
189
190
  /**
191
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
192
   * the markdown AST.
193
   *
194
   * @return
195
   */
196
  private HyperlinkModel getHyperlink() {
197
    final StyleClassedTextArea textArea = getEditor();
198
    final String selectedText = textArea.getSelectedText();
199
200
    // Get the current paragraph, convert to Markdown nodes.
201
    final MarkdownProcessor mp = new MarkdownProcessor( null );
202
    final int p = textArea.getCurrentParagraph();
203
    final String paragraph = textArea.getText( p );
204
    final Node node = mp.toNode( paragraph );
205
    final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() );
206
    final Link link = visitor.process( node );
207
208
    if( link != null ) {
209
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
210
    }
211
212
    final HyperlinkModel model = createHyperlinkModel(
213
      link, selectedText, "https://website.com"
214
    );
215
216
    return model;
217
  }
218
219
  private HyperlinkModel createHyperlinkModel(
220
    final Link link, final String selection, final String url ) {
221
222
    return link == null
223
      ? new HyperlinkModel( selection, url )
224
      : new HyperlinkModel( link );
225
  }
226
227
  private Path getParentPath() {
228
    final Path parentPath = getPath();
229
    return (parentPath != null) ? parentPath.getParent() : null;
230
  }
231
232
  private Dialog<String> createLinkDialog() {
233
    return new LinkDialog( getWindow(), getHyperlink(), getParentPath() );
234
  }
235
236
  private Dialog<String> createImageDialog() {
237
    return new ImageDialog( getWindow(), getParentPath() );
238
  }
239
240
  private void insertObject( final Dialog<String> dialog ) {
241
    dialog.showAndWait().ifPresent( result -> {
242
      getEditor().replaceSelection( result );
243
    } );
244
  }
245
246
  public void insertLink() {
247
    insertObject( createLinkDialog() );
248
  }
249
250
  public void insertImage() {
251
    insertObject( createImageDialog() );
252
  }
253
254
  private Window getWindow() {
255
    return getScrollPane().getScene().getWindow();
256
  }
257
}
1258
A src/main/java/com/scrivenvar/predicates/files/FileTypePredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.files;
29
30
import java.io.File;
31
import java.nio.file.FileSystems;
32
import java.nio.file.PathMatcher;
33
import java.util.Collection;
34
import java.util.function.Predicate;
35
36
/**
37
 * Responsible for testing whether a given path (to a file) matches one of the
38
 * filename extension patterns provided during construction.
39
 *
40
 * @see http://docs.oracle.com/javase/tutorial/essential/io/find.html
41
 *
42
 * @author White Magic Software, Ltd.
43
 */
44
public class FileTypePredicate implements Predicate<File> {
45
46
  private final PathMatcher matcher;
47
48
  /**
49
   * Constructs a new instance given a set of file extension globs.
50
   *
51
   * @param patterns Comma-separated list of globbed extensions including the
52
   * Kleene star (e.g., <code>*.md,*.markdown,*.txt</code>).
53
   */
54
  public FileTypePredicate( final String patterns ) {
55
    this.matcher = FileSystems.getDefault().getPathMatcher(
56
      "glob:**/{" + patterns + "}"
57
    );
58
  }
59
60
  /**
61
   * Constructs a new instance given a list of file extension globs, each must
62
   * include the Kleene star (a.k.a. asterisk).
63
   *
64
   * @param patterns Collection of globbed extensions.
65
   */
66
  public FileTypePredicate( final Collection<String> patterns ) {
67
    this( String.join( ",", patterns ) );
68
  }
69
70
  /**
71
   * Returns true if the file matches the patterns defined during construction.
72
   *
73
   * @param file The filename to match against the given glob patterns.
74
   *
75
   * @return false The filename does not match the glob patterns.
76
   */
77
  @Override
78
  public boolean test( final File file ) {
79
    return getMatcher().matches( file.toPath() );
80
  }
81
82
  private PathMatcher getMatcher() {
83
    return this.matcher;
84
  }
85
}
186
A src/main/java/com/scrivenvar/predicates/strings/ContainsPredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.strings;
29
30
/**
31
 * Determines if one string contains another.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class ContainsPredicate extends StringPredicate {
36
37
  /**
38
   * Calls the superclass to construct the instance.
39
   *
40
   * @param comparate Not null.
41
   */
42
  public ContainsPredicate( final String comparate ) {
43
    super( comparate );
44
  }
45
46
  /**
47
   * Answers whether the given strings match each other. What match means will
48
   * depend on user preferences. The empty condition is required to return the
49
   * first node in a list of child nodes when the user has not yet selected a
50
   * node.
51
   *
52
   * @param comparator The string to compare against the comparate.
53
   *
54
   * @return true if s1 and s2 are a match according to some criteria,or s2 is
55
   * empty.
56
   */
57
  @Override
58
  public boolean test( final String comparator ) {
59
    final String comparate = getComparate().toLowerCase();
60
    return comparator.contains( comparate.toLowerCase() )
61
      || comparate.isEmpty();
62
  }
63
}
164
A src/main/java/com/scrivenvar/predicates/strings/EqualPredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.strings;
29
30
/**
31
 * Determines if two strings are equal.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class EqualPredicate extends StringPredicate {
36
37
  /**
38
   * Calls the superclass to construct the instance.
39
   *
40
   * @param comparate Not null.
41
   */
42
  public EqualPredicate( final String comparate ) {
43
    super( comparate );
44
  }
45
46
  /**
47
   * Compares two strings.
48
   *
49
   * @param comparator A non-null string, possibly empty.
50
   *
51
   * @return true The strings are equal, ignoring case.
52
   */
53
  @Override
54
  public boolean test( final String comparator ) {
55
    return comparator.equalsIgnoreCase( getComparate() );
56
  }
57
}
158
A src/main/java/com/scrivenvar/predicates/strings/StartsPredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.strings;
29
30
/**
31
 * Determines if a string starts with another.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class StartsPredicate extends StringPredicate {
36
37
  /**
38
   * Constructs a new instance using a comparate that will be compared with
39
   * the comparator during the test.
40
   *
41
   * @param comparate The string to compare against the comparator.
42
   */
43
  public StartsPredicate( final String comparate ) {
44
    super( comparate );
45
  }
46
47
  /**
48
   * Compares two strings.
49
   *
50
   * @param comparator A non-null string, possibly empty.
51
   *
52
   * @return true The comparator starts with the comparate, ignoring case.
53
   */
54
  @Override
55
  public boolean test( final String comparator ) {
56
    final String comparate = getComparate().toLowerCase();
57
    return comparator.startsWith( comparate.toLowerCase() );
58
  }
59
}
160
A src/main/java/com/scrivenvar/predicates/strings/StringPredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.strings;
29
30
import java.util.function.Predicate;
31
32
/**
33
 * General predicate for different types of string comparisons.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public abstract class StringPredicate implements Predicate<String> {
38
39
  private final String comparate;
40
41
  public StringPredicate( final String comparate ) {
42
    this.comparate = comparate;
43
  }
44
45
  protected String getComparate() {
46
    return this.comparate;
47
  }
48
}
149
A src/main/java/com/scrivenvar/preferences/FilePreferences.java
1
/*
2
 * Copyright 2016 David Croft and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preferences;
29
30
import java.io.File;
31
import java.io.FileInputStream;
32
import java.io.FileOutputStream;
33
import java.io.IOException;
34
import java.util.ArrayList;
35
import java.util.Enumeration;
36
import java.util.List;
37
import java.util.Map;
38
import java.util.Properties;
39
import java.util.TreeMap;
40
import java.util.prefs.AbstractPreferences;
41
import java.util.prefs.BackingStoreException;
42
43
/**
44
 * Preferences implementation that stores to a user-defined file. Local file
45
 * storage is preferred over a certain operating system's monolithic trash heap
46
 * called a registry. When the OS is locked down, the default Preferences
47
 * implementation will try to write to the registry and fail due to permissions
48
 * problems. This class sidesteps the issue entirely by writing to the user's
49
 * home directory, where permissions should be a bit more lax.
50
 * 
51
 * @see http://stackoverflow.com/q/208231/59087
52
 */
53
public class FilePreferences extends AbstractPreferences {
54
55
  private Map<String, String> root = new TreeMap<>();
56
  private Map<String, FilePreferences> children = new TreeMap<>();
57
  private boolean isRemoved;
58
59
  public FilePreferences( final AbstractPreferences parent, final String name ) {
60
    super( parent, name );
61
62
    try {
63
      sync();
64
    } catch( final BackingStoreException ex ) {
65
      error( ex );
66
    }
67
  }
68
69
  @Override
70
  protected void putSpi( final String key, final String value ) {
71
    root.put( key, value );
72
73
    try {
74
      flush();
75
    } catch( final BackingStoreException ex ) {
76
      error( ex );
77
    }
78
  }
79
80
  @Override
81
  protected String getSpi( final String key ) {
82
    return root.get( key );
83
  }
84
85
  @Override
86
  protected void removeSpi( final String key ) {
87
    root.remove( key );
88
89
    try {
90
      flush();
91
    } catch( final BackingStoreException ex ) {
92
      error( ex );
93
    }
94
  }
95
96
  @Override
97
  protected void removeNodeSpi() throws BackingStoreException {
98
    isRemoved = true;
99
    flush();
100
  }
101
102
  @Override
103
  protected String[] keysSpi() throws BackingStoreException {
104
    return root.keySet().toArray( new String[ root.keySet().size() ] );
105
  }
106
107
  @Override
108
  protected String[] childrenNamesSpi() throws BackingStoreException {
109
    return children.keySet().toArray( new String[ children.keySet().size() ] );
110
  }
111
112
  @Override
113
  protected FilePreferences childSpi( final String name ) {
114
    FilePreferences child = children.get( name );
115
116
    if( child == null || child.isRemoved() ) {
117
      child = new FilePreferences( this, name );
118
      children.put( name, child );
119
    }
120
121
    return child;
122
  }
123
124
  @Override
125
  protected void syncSpi() throws BackingStoreException {
126
    if( isRemoved() ) {
127
      return;
128
    }
129
130
    final File file = FilePreferencesFactory.getPreferencesFile();
131
132
    if( !file.exists() ) {
133
      return;
134
    }
135
136
    synchronized( file ) {
137
      final Properties p = new Properties();
138
139
      try {
140
        p.load( new FileInputStream( file ) );
141
142
        final String path = getPath();
143
        final Enumeration<?> pnen = p.propertyNames();
144
145
        while( pnen.hasMoreElements() ) {
146
          final String propKey = (String)pnen.nextElement();
147
148
          if( propKey.startsWith( path ) ) {
149
            final String subKey = propKey.substring( path.length() );
150
151
            // Only load immediate descendants
152
            if( subKey.indexOf( '.' ) == -1 ) {
153
              root.put( subKey, p.getProperty( propKey ) );
154
            }
155
          }
156
        }
157
      } catch( final IOException ex ) {
158
        error( new BackingStoreException( ex ) );
159
      }
160
    }
161
  }
162
163
  private String getPath() {
164
    final FilePreferences parent = (FilePreferences)parent();
165
166
    return parent == null ? "" : parent.getPath() + name() + '.';
167
  }
168
169
  @Override
170
  protected void flushSpi() throws BackingStoreException {
171
    final File file = FilePreferencesFactory.getPreferencesFile();
172
173
    synchronized( file ) {
174
      final Properties p = new Properties();
175
176
      try {
177
        final String path = getPath();
178
179
        if( file.exists() ) {
180
          p.load( new FileInputStream( file ) );
181
182
          final List<String> toRemove = new ArrayList<>();
183
184
          // Make a list of all direct children of this node to be removed
185
          final Enumeration<?> pnen = p.propertyNames();
186
187
          while( pnen.hasMoreElements() ) {
188
            String propKey = (String)pnen.nextElement();
189
            if( propKey.startsWith( path ) ) {
190
              final String subKey = propKey.substring( path.length() );
191
192
              // Only do immediate descendants
193
              if( subKey.indexOf( '.' ) == -1 ) {
194
                toRemove.add( propKey );
195
              }
196
            }
197
          }
198
199
          // Remove them now that the enumeration is done with
200
          for( final String propKey : toRemove ) {
201
            p.remove( propKey );
202
          }
203
        }
204
205
        // If this node hasn't been removed, add back in any values
206
        if( !isRemoved ) {
207
          for( final String s : root.keySet() ) {
208
            p.setProperty( path + s, root.get( s ) );
209
          }
210
        }
211
212
        p.store( new FileOutputStream( file ), "FilePreferences" );
213
      } catch( final IOException ex ) {
214
        error( new BackingStoreException( ex ) );
215
      }
216
    }
217
  }
218
219
  private void error( final BackingStoreException ex ) {
220
    throw new RuntimeException( ex );
221
  }
222
}
1223
A src/main/java/com/scrivenvar/preferences/FilePreferencesFactory.java
1
/*
2
 * Copyright 2016 David Croft and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preferences;
29
30
import static com.scrivenvar.Constants.APP_TITLE;
31
import java.io.File;
32
import java.nio.file.FileSystems;
33
import java.util.prefs.Preferences;
34
import java.util.prefs.PreferencesFactory;
35
36
/**
37
 * PreferencesFactory implementation that stores the preferences in a
38
 * user-defined file. Usage:
39
 * <pre>
40
 * System.setProperty( "java.util.prefs.PreferencesFactory",
41
 * FilePreferencesFactory.class.getName() );
42
 * </pre>
43
 * <p>
44
 * The file defaults to <code>$user.home/.scrivenvar</code>, but can be changed
45
 * using <code>-Dapplication.name=preferences</code> when running the
46
 * application, or by calling <code>System.setProperty</code> with the
47
 * "application.name" property.
48
 * </p>
49
 */
50
public class FilePreferencesFactory implements PreferencesFactory {
51
52
  private static File preferencesFile;
53
  private Preferences rootPreferences;
54
55
  @Override
56
  public Preferences systemRoot() {
57
    return userRoot();
58
  }
59
60
  @Override
61
  public synchronized Preferences userRoot() {
62
    if( rootPreferences == null ) {
63
      rootPreferences = new FilePreferences( null, "" );
64
    }
65
66
    return rootPreferences;
67
  }
68
69
  public synchronized static File getPreferencesFile() {
70
    if( preferencesFile == null ) {
71
      String prefsFile = getPreferencesFilename();
72
73
      preferencesFile = new File( prefsFile ).getAbsoluteFile();
74
    }
75
76
    return preferencesFile;
77
  }
78
79
  public static String getPreferencesFilename() {
80
    final String filename = System.getProperty( "application.name", APP_TITLE );
81
    return System.getProperty( "user.home" ) + getSeparator() + "." + filename;
82
  }
83
84
  public static String getSeparator() {
85
    return FileSystems.getDefault().getSeparator();
86
  }
87
}
188
A src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import static com.scrivenvar.Constants.CARET_POSITION_BASE;
31
import static com.scrivenvar.Constants.STYLESHEET_PREVIEW;
32
import java.nio.file.Path;
33
import javafx.beans.value.ObservableValue;
34
import javafx.concurrent.Worker.State;
35
import static javafx.concurrent.Worker.State.SUCCEEDED;
36
import javafx.scene.Node;
37
import javafx.scene.layout.Pane;
38
import javafx.scene.web.WebEngine;
39
import javafx.scene.web.WebView;
40
41
/**
42
 * HTML preview pane is responsible for rendering an HTML document.
43
 *
44
 * @author Karl Tauber and White Magic Software, Ltd.
45
 */
46
public final class HTMLPreviewPane extends Pane {
47
48
  private final WebView webView = new WebView();
49
  private Path path;
50
51
  /**
52
   * Creates a new preview pane that can scroll to the caret position within the
53
   * document.
54
   */
55
  public HTMLPreviewPane() {
56
    initListeners();
57
    initTraversal();
58
  }
59
60
  /**
61
   * Initializes observers for document changes. When the document is reloaded
62
   * with new HTML, this triggers a scroll event that repositions the document
63
   * to the injected caret (that corresponds with the position in the text
64
   * editor).
65
   */
66
  private void initListeners() {
67
    // Scrolls to the caret after the content has been loaded.
68
    getEngine().getLoadWorker().stateProperty().addListener(
69
      (ObservableValue<? extends State> observable,
70
        final State oldValue, final State newValue) -> {
71
        if( newValue == SUCCEEDED ) {
72
          scrollToCaret();
73
        }
74
      } );
75
  }
76
77
  /**
78
   * Ensures images can be found relative to the document.
79
   *
80
   * @return The base path element to use for the document, or the empty string
81
   * if no path has been set, yet.
82
   */
83
  private String getBase() {
84
    final Path basePath = getPath();
85
86
    return basePath == null
87
      ? ""
88
      : ("<base href='" + basePath.getParent().toUri().toString() + "'>");
89
  }
90
91
  /**
92
   * Updates the internal HTML source, loads it into the preview pane, then
93
   * scrolls to the caret position.
94
   *
95
   * @param html The new HTML document to display.
96
   */
97
  public void update( final String html ) {
98
    getEngine().loadContent(
99
      "<!DOCTYPE html>"
100
      + "<html>"
101
      + "<head>"
102
      + "<link rel='stylesheet' href='" + getClass().getResource( STYLESHEET_PREVIEW ) + "'>"
103
      + getBase()
104
      + "</head>"
105
      + "<body>"
106
      + html
107
      + "</body>"
108
      + "</html>" );
109
  }
110
111
  /**
112
   * Clears out the HTML content from the preview.
113
   */
114
  public void clear() {
115
    update( "" );
116
  }
117
118
  /**
119
   * Scrolls to the caret position in the document.
120
   */
121
  private void scrollToCaret() {
122
    execute( getScrollScript() );
123
  }
124
125
  /**
126
   * Returns the JavaScript used to scroll the WebView pane.
127
   *
128
   * @return A script that tries to center the view port on the CARET POSITION.
129
   */
130
  private String getScrollScript() {
131
    return ""
132
      + "var e = document.getElementById('" + CARET_POSITION_BASE + "');"
133
      + "if( e != null ) { "
134
      + "  Element.prototype.topOffset = function () {"
135
      + "    return this.offsetTop + (this.offsetParent ? this.offsetParent.topOffset() : 0);"
136
      + "  };"
137
      + "  window.scrollTo( 0, e.topOffset() - (window.innerHeight / 2 ) );"
138
      + "}";
139
  }
140
141
  /**
142
   * Prevent tabbing into the preview pane.
143
   */
144
  private void initTraversal() {
145
    getWebView().setFocusTraversable( false );
146
  }
147
148
  private Object execute( final String script ) {
149
    return getEngine().executeScript( script );
150
  }
151
152
  private WebEngine getEngine() {
153
    return getWebView().getEngine();
154
  }
155
156
  private WebView getWebView() {
157
    return this.webView;
158
  }
159
160
  private Path getPath() {
161
    return this.path;
162
  }
163
164
  public void setPath( final Path path ) {
165
    this.path = path;
166
  }
167
168
  /**
169
   * Content to embed in a panel.
170
   *
171
   * @return The content to display to the user.
172
   */
173
  public Node getNode() {
174
    return getWebView();
175
  }
176
}
1177
A src/main/java/com/scrivenvar/processors/AbstractProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
/**
31
 * Responsible for transforming a document through a variety of chained
32
 * handlers. If there are conditions where this handler should not process the
33
 * entire chain, create a second handler, or split the chain into reusable
34
 * sub-chains.
35
 *
36
 * @author White Magic Software, Ltd.
37
 * @param <T> The type of object to process.
38
 */
39
public abstract class AbstractProcessor<T> implements Processor<T> {
40
41
  protected static final char NEWLINE = '\n';
42
43
  /**
44
   * When performing string searches using indexOf, a return value of -1
45
   * indicates that the string could not be found.
46
   */
47
  protected static final int INDEX_NOT_FOUND = -1;
48
49
  /**
50
   * Used while processing the entire chain; null to signify no more links.
51
   */
52
  private final Processor<T> next;
53
54
  /**
55
   * Constructs a succession without a successor (i.e., next is null).
56
   */
57
  protected AbstractProcessor() {
58
    this( null );
59
  }
60
61
  /**
62
   * Constructs a new default handler with a given successor.
63
   *
64
   * @param successor Use null to indicate last link in the chain.
65
   */
66
  public AbstractProcessor( final Processor<T> successor ) {
67
    this.next = successor;
68
  }
69
70
  /**
71
   * Processes links in the chain while there are successors and valid data to
72
   * process.
73
   *
74
   * @param t The object to process.
75
   */
76
  @Override
77
  public synchronized void processChain( T t ) {
78
    Processor<T> handler = this;
79
80
    while( handler != null && t != null ) {
81
      t = handler.processLink( t );
82
      handler = handler.next();
83
    }
84
  }
85
86
  @Override
87
  public Processor<T> next() {
88
    return this.next;
89
  }
90
}
191
A src/main/java/com/scrivenvar/processors/CaretInsertionProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static com.scrivenvar.Constants.CARET_POSITION_MD;
31
import javafx.beans.property.IntegerProperty;
32
import javafx.beans.property.SimpleIntegerProperty;
33
import javafx.beans.value.ObservableValue;
34
35
/**
36
 * Base class for inserting the magic CARET POSITION into the text so that, upon
37
 * previewing, the preview pane can scroll to the correct position (relative to
38
 * the caret position in the editor).
39
 *
40
 * @author White Magic Software, Ltd.
41
 */
42
public abstract class CaretInsertionProcessor extends AbstractProcessor<String> {
43
44
  private final IntegerProperty caretPosition = new SimpleIntegerProperty();
45
  private final static String NEWLINE_CARET_POSITION_MD = NEWLINE + CARET_POSITION_MD;
46
47
  public CaretInsertionProcessor(
48
    final Processor<String> processor,
49
    final ObservableValue<Integer> position ) {
50
    super( processor );
51
    this.caretPosition.bind( position );
52
  }
53
54
  /**
55
   * Inserts the caret position token into the text at an offset that won't
56
   * interfere with parsing the text itself, regardless of text format.
57
   *
58
   * @param text The text document to change.
59
   * @param i The caret position token insertion point to use, or -1 to return
60
   * the text without any injection.
61
   *
62
   * @return The given text with a caret position token inserted at the given
63
   * offset.
64
   */
65
  protected String inject( final String text, final int i ) {
66
    if( i > 0 && i <= text.length() ) {
67
      // Preserve the newline character when inserting the caret position mark.
68
      final String replacement = text.charAt( i - 1 ) == NEWLINE
69
        ? NEWLINE_CARET_POSITION_MD
70
        : CARET_POSITION_MD;
71
72
      return new StringBuilder( text ).replace( i, i, replacement ).toString();
73
    }
74
75
    return text;
76
  }
77
78
  /**
79
   * Returns true if i is greater than or equal to min and less than or equal to
80
   * max.
81
   *
82
   * @param i The value to check.
83
   * @param min The lower bound.
84
   * @param max The upper bound.
85
   *
86
   * @return false The value of i is either lower than min or greater than max.
87
   */
88
  protected boolean isBetween( int i, int min, int max ) {
89
    return i >= min && i <= max;
90
  }
91
92
  /**
93
   * Returns the editor's caret position.
94
   *
95
   * @return Where the user has positioned the caret.
96
   */
97
  protected int getCaretPosition() {
98
    return this.caretPosition.getValue();
99
  }
100
}
1101
A src/main/java/com/scrivenvar/processors/CaretReplacementProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static com.scrivenvar.Constants.CARET_POSITION_HTML;
31
import static com.scrivenvar.Constants.CARET_POSITION_MD;
32
33
/**
34
 * Responsible for replacing the caret position marker with an HTML element
35
 * suitable to use as a reference for scrolling a view port.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public class CaretReplacementProcessor extends AbstractProcessor<String> {
40
41
  public CaretReplacementProcessor( final Processor<String> processor ) {
42
    super( processor );
43
  }
44
45
  /**
46
   * Replaces each MD_CARET_POSITION with an HTML element that has an id
47
   * attribute of CARET_POSITION. This should only replace one item.
48
   *
49
   * @param t The text that contains
50
   *
51
   * @return
52
   */
53
  @Override
54
  public String processLink( final String t ) {
55
    return replace( t, CARET_POSITION_MD, CARET_POSITION_HTML );
56
  }
57
58
  /**
59
   * Replaces the needle with thread in the given haystack. Based on Apache
60
   * Commons 3 StringUtils.replace method. Should be faster than String.replace,
61
   * which performs a little regex under the hood.
62
   *
63
   * @param haystack Search this string for the needle, must not be null.
64
   * @param needle The text to find in the haystack.
65
   * @param thread Replace the needle with this text, if the needle is found.
66
   *
67
   * @return The haystack with the first instance of needle replaced with
68
   * thread.
69
   */
70
  private static String replace(
71
    final String haystack, final String needle, final String thread ) {
72
73
    final int end = haystack.indexOf( needle, 0 );
74
75
    if( end == INDEX_NOT_FOUND ) {
76
      return haystack;
77
    }
78
79
    int start = 0;
80
    final int needleLength = needle.length();
81
82
    int len = thread.length() - needleLength;
83
    len = (len < 0 ? 0 : len);
84
    final StringBuilder buffer = new StringBuilder( haystack.length() + len );
85
86
    if( end != INDEX_NOT_FOUND ) {
87
      buffer.append( haystack.substring( start, end ) ).append( thread );
88
      start = end + needleLength;
89
    }
90
91
    return buffer.append( haystack.substring( start ) ).toString();
92
  }
93
}
194
A src/main/java/com/scrivenvar/processors/DefaultVariableProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
31
import java.util.Map;
32
33
/**
34
 * Processes variables in the document and inserts their values into the
35
 * post-processed text. The default variable syntax is <code>$variable$</code>.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public class DefaultVariableProcessor extends AbstractProcessor<String> {
40
41
  private Map<String, String> definitions;
42
43
  /**
44
   * Constructs a variable processor for dereferencing variables.
45
   *
46
   * @param successor Usually the HTML Preview Processor.
47
   */
48
  private DefaultVariableProcessor( final Processor<String> successor ) {
49
    super( successor );
50
  }
51
52
  public DefaultVariableProcessor(
53
    final Processor<String> successor, final Map<String, String> map ) {
54
    this( successor );
55
    setDefinitions( map );
56
  }
57
58
  /**
59
   * Processes the given text document by replacing variables with their values.
60
   *
61
   * @param text The document text that includes variables that should be
62
   * replaced with values when rendered as HTML.
63
   *
64
   * @return The text with all variables replaced.
65
   */
66
  @Override
67
  public String processLink( final String text ) {
68
    return replace( text, getDefinitions() );
69
  }
70
71
  /**
72
   * Returns the map to use for variable substitution.
73
   *
74
   * @return A map of variable names to values.
75
   */
76
  protected Map<String, String> getDefinitions() {
77
    return this.definitions;
78
  }
79
80
  private void setDefinitions( final Map<String, String> definitions ) {
81
    this.definitions = definitions;
82
  }
83
}
184
A src/main/java/com/scrivenvar/processors/HTMLPreviewProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.preview.HTMLPreviewPane;
31
32
/**
33
 * Responsible for notifying the HTMLPreviewPane when the succession chain has
34
 * updated. This decouples knowledge of changes to the editor panel from the
35
 * HTML preview panel as well as any processing that takes place before the
36
 * final HTML preview is rendered. This should be the last link in the processor
37
 * chain.
38
 *
39
 * @author White Magic Software, Ltd.
40
 */
41
public class HTMLPreviewProcessor extends AbstractProcessor<String> {
42
43
  private HTMLPreviewPane htmlPreviewPane;
44
45
  /**
46
   * Constructs the end of a processing chain.
47
   *
48
   * @param htmlPreviewPane The pane to update with the post-processed document.
49
   */
50
  public HTMLPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) {
51
    super( null );
52
    setHtmlPreviewPane( htmlPreviewPane );
53
  }
54
55
  /**
56
   * Update the preview panel using HTML from the succession chain.
57
   *
58
   * @param html The document content to render in the preview pane. The HTML
59
   * should not contain a doctype, head, or body tag, only content to render
60
   * within the body.
61
   *
62
   * @return null
63
   */
64
  @Override
65
  public String processLink( final String html ) {
66
    getHtmlPreviewPane().update( html );
67
68
    // No more processing required.
69
    return null;
70
  }
71
72
  private HTMLPreviewPane getHtmlPreviewPane() {
73
    return this.htmlPreviewPane;
74
  }
75
76
  private void setHtmlPreviewPane( final HTMLPreviewPane htmlPreviewPane ) {
77
    this.htmlPreviewPane = htmlPreviewPane;
78
  }
79
}
180
A src/main/java/com/scrivenvar/processors/IdentityProcessor.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
/**
31
 * This is the default processor used when an unknown filename extension is
32
 * encountered.
33
 *
34
 * @author White Magic Software, Ltd.
35
 */
36
public class IdentityProcessor extends AbstractProcessor<String> {
37
38
  /**
39
   * Passes the link to the super constructor.
40
   * 
41
   * @param link The next processor in the chain to use for text processing.
42
   */
43
  public IdentityProcessor( final Processor<String> link ) {
44
    super( link );
45
  }
46
47
  /**
48
   * Returns the given string, modified with "pre" tags.
49
   *
50
   * @param t The string to return, enclosed in "pre" tags.
51
   *
52
   * @return t
53
   */
54
  @Override
55
  public String processLink( final String t ) {
56
    final StringBuilder result = new StringBuilder( t.length() + 16 );
57
    
58
    return result.append( "<pre>" ).append( t ).append( "</pre>" ).toString();
59
  }
60
}
161
A src/main/java/com/scrivenvar/processors/InlineRProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static com.scrivenvar.Constants.PERSIST_R_STARTUP;
31
import static com.scrivenvar.Constants.STATUS_PARSE_ERROR;
32
import static com.scrivenvar.Messages.get;
33
import com.scrivenvar.Services;
34
import static com.scrivenvar.decorators.RVariableDecorator.PREFIX;
35
import static com.scrivenvar.decorators.RVariableDecorator.SUFFIX;
36
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
37
import com.scrivenvar.service.Options;
38
import com.scrivenvar.service.events.Notifier;
39
import java.io.IOException;
40
import static java.lang.Math.min;
41
import java.nio.file.Path;
42
import java.nio.file.Paths;
43
import java.util.Map;
44
import javax.script.ScriptEngine;
45
import javax.script.ScriptEngineManager;
46
import javax.script.ScriptException;
47
48
/**
49
 * Transforms a document containing R statements into Markdown.
50
 *
51
 * @author White Magic Software, Ltd.
52
 */
53
public final class InlineRProcessor extends DefaultVariableProcessor {
54
55
  private final Notifier notifier = Services.load( Notifier.class );
56
  private final Options options = Services.load( Options.class );
57
58
  private ScriptEngine engine;
59
60
  /**
61
   * Constructs a processor capable of evaluating R statements.
62
   *
63
   * @param processor Subsequent link in the processing chain.
64
   * @param map Resolved definitions map.
65
   * @param path Path to the file being edited so that its working directory can
66
   * be extracted. Must not be null.
67
   */
68
  public InlineRProcessor(
69
    final Processor<String> processor,
70
    final Map<String, String> map,
71
    final Path path ) {
72
    super( processor, map );
73
    init( path.getParent() );
74
  }
75
76
  /**
77
   *
78
   * @param workingDirectory
79
   */
80
  private void init( final Path workingDirectory ) {
81
    try {
82
      final Path wd = nullSafe( workingDirectory );
83
      final String dir = wd.toString().replace( '\\', '/' );
84
      final Map<String, String> definitions = getDefinitions();
85
      definitions.put( "$application.r.working.directory$", dir );
86
87
      final String initScript = getInitScript();
88
89
      if( !initScript.isEmpty() ) {
90
        final String rScript = replace( initScript, getDefinitions() );
91
        eval( rScript );
92
      }
93
    } catch( final IOException | ScriptException e ) {
94
      throw new RuntimeException( e );
95
    }
96
  }
97
98
  private String getInitScript() throws IOException {
99
    return getOptions().get( PERSIST_R_STARTUP, "" );
100
  }
101
102
  @Override
103
  public String processLink( final String text ) {
104
    final int length = text.length();
105
    final int prefixLength = PREFIX.length();
106
107
    // Pre-allocate the same amount of space. A calculation is longer to write
108
    // than its computed value inserted into the text.
109
    final StringBuilder sb = new StringBuilder( length );
110
111
    int prevIndex = 0;
112
    int currIndex = text.indexOf( PREFIX );
113
114
    while( currIndex >= 0 ) {
115
      // Copy everything up to, but not including, an R statement (`r#).
116
      sb.append( text.substring( prevIndex, currIndex ) );
117
118
      // Jump to the start of the R statement.
119
      prevIndex = currIndex + prefixLength;
120
121
      // Find the statement ending (`), without indexing past the text boundary.
122
      currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
123
124
      // Only evalutate inline R statements that have end delimiters.
125
      if( currIndex > 1 ) {
126
        // Extract the inline R statement to be evaluated.
127
        final String r = text.substring( prevIndex, currIndex );
128
129
        // Pass the R statement into the R engine for evaluation.
130
        try {
131
          final Object result = eval( r );
132
133
          // Append the string representation of the result into the text.
134
          sb.append( result );
135
        } catch( final Exception e ) {
136
          // If the string couldn't be parsed using R, append the statement
137
          // that failed to parse, instead of its evaluated value.
138
          sb.append( PREFIX ).append( r ).append( SUFFIX );
139
140
          // Tell the user that there was a problem.
141
          getNotifier().notify( get( STATUS_PARSE_ERROR,
142
            e.getMessage(), currIndex )
143
          );
144
        }
145
146
        // Retain the R statement's ending position in the text.
147
        prevIndex = currIndex + 1;
148
      }
149
      else {
150
        // TODO: Implement this.
151
        // There was a starting prefix but no ending suffix. Ignore the
152
        // problem, copy to the end, and exit the loop.
153
        //sb.append()
154
      }
155
156
      // Find the start of the next inline R statement.
157
      currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) );
158
    }
159
160
    // Copy from the previous index to the end of the string.
161
    return sb.append( text.substring( min( prevIndex, length ) ) ).toString();
162
  }
163
164
  /**
165
   * Evaluate an R expression and return the resulting object.
166
   *
167
   * @param r The expression to evaluate.
168
   *
169
   * @return The object resulting from the evaluation.
170
   */
171
  private Object eval( final String r ) throws ScriptException {
172
    return getScriptEngine().eval( r );
173
  }
174
175
  private synchronized ScriptEngine getScriptEngine() {
176
    if( this.engine == null ) {
177
      this.engine = (new ScriptEngineManager()).getEngineByName( "Renjin" );
178
    }
179
180
    return this.engine;
181
  }
182
183
  private Notifier getNotifier() {
184
    return this.notifier;
185
  }
186
187
  private Options getOptions() {
188
    return this.options;
189
  }
190
191
  /**
192
   * This will return the given path if not null, otherwise it will return
193
   * Paths.get( System.getProperty( "user.dir" ) ).
194
   *
195
   * @param path The path to make null safe.
196
   *
197
   * @return A non-null path.
198
   */
199
  private Path nullSafe( final Path path ) {
200
    return path == null ? Paths.get( System.getProperty( "user.dir" ) ) : path;
201
  }
202
}
1203
A src/main/java/com/scrivenvar/processors/MarkdownCaretInsertionProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static java.lang.Character.isLetter;
31
import static java.lang.Math.min;
32
import javafx.beans.value.ObservableValue;
33
34
/**
35
 * Responsible for inserting a caret position token into a markdown document.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public class MarkdownCaretInsertionProcessor extends CaretInsertionProcessor {
40
41
  /**
42
   * Constructs a processor capable of inserting a caret marker into Markdown.
43
   *
44
   * @param processor The next processor in the chain.
45
   * @param position The caret's current position in the text.
46
   */
47
  public MarkdownCaretInsertionProcessor(
48
    final Processor<String> processor,
49
    final ObservableValue<Integer> position ) {
50
    super( processor, position );
51
  }
52
53
  /**
54
   * Changes the text to insert a "caret" at the caret position. This will
55
   * insert the unique key of Constants.MD_CARET_POSITION into the document.
56
   *
57
   * @param t The text document to process.
58
   *
59
   * @return The text with the caret position token inserted at the caret
60
   * position.
61
   */
62
  @Override
63
  public String processLink( final String t ) {
64
    final int length = t.length();
65
    int offset = min( getCaretPosition(), length );
66
67
    // TODO: Ensure that the caret position is outside of an element, 
68
    // so that a caret inserted in the image doesn't corrupt it. Such as:
69
    //
70
    // ![Screenshot](images/scr|eenshot.png)
71
    //
72
    // 1. Scan back to the previous EOL, which will be the MD AST start point.
73
    // 2. Scan forward until EOF or EOL, which will be the MD AST ending point.
74
    // 3. Convert the text between start and end into MD AST.
75
    // 4. Find the nearest text node to the caret.
76
    // 5. Insert the CARET_POSITION_MD value in the text at that offsset.
77
    // Insert the caret at the closest non-Markdown delimiter (i.e., the 
78
    // closest character from the caret position forward).
79
    while( offset < length && !isLetter( t.charAt( offset ) ) ) {
80
      offset++;
81
    }
82
83
    return inject( t, offset );
84
  }
85
}
186
A src/main/java/com/scrivenvar/processors/MarkdownProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.vladsch.flexmark.Extension;
31
import com.vladsch.flexmark.ast.Node;
32
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
33
import com.vladsch.flexmark.ext.gfm.tables.TablesExtension;
34
import com.vladsch.flexmark.html.HtmlRenderer;
35
import com.vladsch.flexmark.parser.Parser;
36
import com.vladsch.flexmark.superscript.SuperscriptExtension;
37
import java.util.ArrayList;
38
import java.util.List;
39
40
41
/**
42
 * Responsible for parsing a Markdown document and rendering it as HTML.
43
 *
44
 * @author White Magic Software, Ltd.
45
 */
46
public class MarkdownProcessor extends AbstractProcessor<String> {
47
48
  private List<Extension> extensions;
49
50
  /**
51
   * Constructs a new Markdown processor that can create HTML documents.
52
   *
53
   * @param successor Usually the HTML Preview Processor.
54
   */
55
  public MarkdownProcessor( final Processor<String> successor ) {
56
    super( successor );
57
  }
58
59
  /**
60
   * Converts the given Markdown string into HTML, without the doctype, html,
61
   * head, and body tags.
62
   *
63
   * @param markdown The string to convert from Markdown to HTML.
64
   *
65
   * @return The HTML representation of the Markdown document.
66
   */
67
  @Override
68
  public String processLink( final String markdown ) {
69
    return toHtml( markdown );
70
  }
71
72
  /**
73
   * Returns the AST in the form of a node for the given markdown document. This
74
   * can be used, for example, to determine if a hyperlink exists inside of a
75
   * paragraph.
76
   *
77
   * @param markdown The markdown to convert into an AST.
78
   *
79
   * @return The markdown AST for the given text (usually a paragraph).
80
   */
81
  public Node toNode( final String markdown ) {
82
    return parse( markdown );
83
  }
84
85
  /**
86
   * Helper method to create an AST given some markdown.
87
   *
88
   * @param markdown The markdown to parse.
89
   *
90
   * @return The root node of the markdown tree.
91
   */
92
  private Node parse( final String markdown ) {
93
    return createParser().parse( markdown );
94
  }
95
96
  /**
97
   * Converts a string of markdown into HTML.
98
   *
99
   * @param markdown The markdown text to convert to HTML, must not be null.
100
   *
101
   * @return The markdown rendered as an HTML document.
102
   */
103
  private String toHtml( final String markdown ) {
104
    return createRenderer().render( parse( markdown ) );
105
  }
106
107
  /**
108
   * Returns the list of extensions to use when parsing and rendering Markdown
109
   * into HTML.
110
   *
111
   * @return A non-null list of Markdown extensions.
112
   */
113
  private synchronized List<Extension> getExtensions() {
114
    if( this.extensions == null ) {
115
      this.extensions = createExtensions();
116
    }
117
118
    return this.extensions;
119
  }
120
121
  /**
122
   * Creates a list that includes a TablesExtension. Subclasses may override
123
   * this method to insert more extensions, or remove the table extension.
124
   *
125
   * @return A list with an extension for parsing and rendering tables.
126
   */
127
  protected List<Extension> createExtensions() {
128
    final List<Extension> result = new ArrayList<>();
129
    result.add( TablesExtension.create() );
130
    result.add( SuperscriptExtension.create() );
131
    result.add( StrikethroughSubscriptExtension.create() );
132
    return result;
133
  }
134
135
  /**
136
   * Creates the Markdown document processor.
137
   *
138
   * @return A Parser that can build an abstract syntax tree.
139
   */
140
  private Parser createParser() {
141
    return Parser.builder().extensions( getExtensions() ).build();
142
  }
143
144
  /**
145
   * Creates the HTML document renderer.
146
   *
147
   * @return A renderer that can convert a Markdown AST to HTML.
148
   */
149
  private HtmlRenderer createRenderer() {
150
    return HtmlRenderer.builder().extensions( getExtensions() ).build();
151
  }
152
}
1153
A src/main/java/com/scrivenvar/processors/Processor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
/**
31
 * Responsible for processing documents from one known format to another.
32
 *
33
 * @author White Magic Software, Ltd.
34
 * @param <T> The type of processor to create.
35
 */
36
public interface Processor<T> {
37
  
38
  /**
39
   * Provided so that the chain can be invoked from any link using a given
40
   * value. This should be called automatically by a superclass so that
41
   * the links in the chain need only implement the processLink method.
42
   * 
43
   * @param t The value to pass along to each link in the chain.
44
   */
45
  public void processChain( T t );
46
47
  /**
48
   * Processes the given content providing a transformation from one document
49
   * format into another. For example, this could convert from XML to text using
50
   * an XSLT processor, or from markdown to HTML.
51
   *
52
   * @param t The type of object to process.
53
   *
54
   * @return The post-processed document, or null if processing should stop.
55
   */
56
  public T processLink( T t );
57
58
  /**
59
   * Adds a document processor to call after this processor finishes processing
60
   * the document given to the process method.
61
   *
62
   * @return The processor that should transform the document after this
63
   * instance has finished processing.
64
   */
65
  public Processor<T> next();
66
}
167
A src/main/java/com/scrivenvar/processors/ProcessorFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.FileEditorTab;
32
import com.scrivenvar.preview.HTMLPreviewPane;
33
import java.nio.file.Path;
34
import java.util.Map;
35
import javafx.beans.value.ObservableValue;
36
37
/**
38
 * Responsible for creating processors capable of parsing, transforming,
39
 * interpolating, and rendering known file types.
40
 *
41
 * @author White Magic Software, Ltd.
42
 */
43
public class ProcessorFactory extends AbstractFileFactory {
44
45
  private final HTMLPreviewPane previewPane;
46
  private final Map<String, String> resolvedMap;
47
48
  private Processor<String> terminalProcessChain;
49
50
  /**
51
   * Constructs a factory with the ability to create processors that can perform
52
   * text and caret processing to generate a final preview.
53
   *
54
   * @param previewPane
55
   * @param resolvedMap
56
   */
57
  public ProcessorFactory(
58
    final HTMLPreviewPane previewPane,
59
    final Map<String, String> resolvedMap ) {
60
    this.previewPane = previewPane;
61
    this.resolvedMap = resolvedMap;
62
  }
63
64
  /**
65
   * Creates a processor suitable for parsing and rendering the file opened at
66
   * the given tab.
67
   *
68
   * @param tab The tab containing a text editor, path, and caret position.
69
   *
70
   * @return A processor that can render the given tab's text.
71
   */
72
  public Processor<String> createProcessor( final FileEditorTab tab ) {
73
    final Path path = tab.getPath();
74
    final Processor<String> processor;
75
76
    switch( lookup( path ) ) {
77
      case RMARKDOWN:
78
        processor = createRProcessor( tab );
79
        break;
80
81
      case MARKDOWN:
82
        processor = createMarkdownProcessor( tab );
83
        break;
84
85
      case XML:
86
        processor = createXMLProcessor( tab );
87
        break;
88
89
      case RXML:
90
        processor = createRXMLProcessor( tab );
91
        break;
92
93
      default:
94
        processor = createIdentityProcessor( tab );
95
        break;
96
    }
97
98
    return processor;
99
  }
100
101
  /**
102
   * Returns a processor common to all processors: markdown, caret position
103
   * token replacer, and an HTML preview renderer.
104
   *
105
   * @return Processors at the end of the processing chain.
106
   */
107
  private Processor<String> getCommonProcessor() {
108
    if( this.terminalProcessChain == null ) {
109
      this.terminalProcessChain = createCommonProcessor();
110
    }
111
112
    return this.terminalProcessChain;
113
  }
114
115
  /**
116
   * Creates and links the processors at the end of the processing chain.
117
   *
118
   * @return A markdown, caret replacement, and preview pane processor chain.
119
   */
120
  private Processor<String> createCommonProcessor() {
121
    final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
122
    final Processor<String> mcrp = new CaretReplacementProcessor( hpp );
123
    final Processor<String> mpp = new MarkdownProcessor( mcrp );
124
125
    return mpp;
126
  }
127
  
128
  protected Processor<String> createIdentityProcessor( final FileEditorTab tab ) {
129
    final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
130
    final Processor<String> ip = new IdentityProcessor( hpp );
131
    
132
    return ip;
133
  }
134
135
  protected Processor<String> createMarkdownProcessor( final FileEditorTab tab ) {
136
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
137
    final Processor<String> tpc = getCommonProcessor();
138
    final Processor<String> cip = createMarkdownInsertionProcessor( tpc, caret );
139
    final Processor<String> dvp = new DefaultVariableProcessor( cip, getResolvedMap() );
140
141
    return dvp;
142
  }
143
144
  protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
145
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
146
    final Processor<String> tpc = getCommonProcessor();
147
    final Processor<String> xmlp = new XMLProcessor( tpc, tab.getPath() );
148
    final Processor<String> dvp = new DefaultVariableProcessor( xmlp, getResolvedMap() );
149
    final Processor<String> xcip = createXMLInsertionProcessor( dvp, caret );
150
151
    return xcip;
152
  }
153
154
  protected Processor<String> createRProcessor( final FileEditorTab tab ) {
155
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
156
    final Processor<String> tpc = getCommonProcessor();
157
    final Processor<String> rp = new InlineRProcessor( tpc, getResolvedMap(), tab.getPath() );
158
    final Processor<String> rvp = new RVariableProcessor( rp, getResolvedMap() );
159
    final Processor<String> cip = createRInsertionProcessor( rvp, caret );
160
161
    return cip;
162
  }
163
164
  protected Processor<String> createRXMLProcessor( final FileEditorTab tab ) {
165
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
166
    final Processor<String> tpc = getCommonProcessor();
167
    final Processor<String> xmlp = new XMLProcessor( tpc, tab.getPath() );
168
    final Processor<String> rp = new InlineRProcessor( xmlp, getResolvedMap(), tab.getPath() );
169
    final Processor<String> rvp = new RVariableProcessor( rp, getResolvedMap() );
170
    final Processor<String> xcip = createXMLInsertionProcessor( rvp, caret );
171
172
    return xcip;
173
  }
174
175
  private Processor<String> createMarkdownInsertionProcessor(
176
    final Processor<String> tpc, final ObservableValue<Integer> caret ) {
177
    return new MarkdownCaretInsertionProcessor( tpc, caret );
178
  }
179
180
  /**
181
   * Create an insertion processor that is aware of R statements and will insert
182
   * a caret outside of any statement the caret falls within.
183
   *
184
   * @param processor Another link in the processor chain.
185
   * @param caret The caret insertion point.
186
   *
187
   * @return A processor that can insert a caret token without disturbing any R
188
   * code.
189
   */
190
  private Processor<String> createRInsertionProcessor(
191
    final Processor<String> processor, final ObservableValue<Integer> caret ) {
192
    return new RMarkdownCaretInsertionProcessor( processor, caret );
193
  }
194
195
  private Processor<String> createXMLInsertionProcessor(
196
    final Processor<String> tpc, final ObservableValue<Integer> caret ) {
197
    return new XMLCaretInsertionProcessor( tpc, caret );
198
  }
199
200
  private HTMLPreviewPane getPreviewPane() {
201
    return this.previewPane;
202
  }
203
204
  /**
205
   * Returns the variable map of interpolated definitions.
206
   *
207
   * @return A map to help dereference variables.
208
   */
209
  private Map<String, String> getResolvedMap() {
210
    return this.resolvedMap;
211
  }
212
}
1213
A src/main/java/com/scrivenvar/processors/RMarkdownCaretInsertionProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static com.scrivenvar.decorators.RVariableDecorator.PREFIX;
31
import static com.scrivenvar.decorators.RVariableDecorator.SUFFIX;
32
import static java.lang.Integer.max;
33
import javafx.beans.value.ObservableValue;
34
35
/**
36
 * Responsible for inserting a caret position token into an R document.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class RMarkdownCaretInsertionProcessor
41
  extends MarkdownCaretInsertionProcessor {
42
43
  /**
44
   * Constructs a processor capable of inserting a caret marker into Markdown.
45
   *
46
   * @param processor The next processor in the chain.
47
   * @param position The caret's current position in the text.
48
   */
49
  public RMarkdownCaretInsertionProcessor(
50
    final Processor<String> processor,
51
    final ObservableValue<Integer> position ) {
52
    super( processor, position );
53
  }
54
55
  /**
56
   * Changes the text to insert a "caret" at the caret position. This will
57
   * insert the unique key of Constants.MD_CARET_POSITION into the document.
58
   *
59
   * @param text The text document to process.
60
   *
61
   * @return The text with the caret position token inserted at the caret
62
   * position.
63
   */
64
  @Override
65
  public String processLink( final String text ) {
66
    int offset = getCaretPosition();
67
68
    // Search for inline R code from the start of the caret's paragraph.
69
    // This should be much faster than scanning text from the beginning.
70
    int index = text.lastIndexOf( NEWLINE, offset );
71
72
    if( index == INDEX_NOT_FOUND ) {
73
      index = 0;
74
    }
75
76
    // Scan for an inline R statement, either from the nearest paragraph or
77
    // the beginning of the file, whichever was found first.
78
    index = text.indexOf( PREFIX, index );
79
80
    // If there was no R prefix then insert at the caret's initial offset...
81
    if( index != INDEX_NOT_FOUND ) {
82
      // Otherwise, retain the starting index of the first R statement in the
83
      // paragraph.
84
      int rPrefix = index + 1;
85
86
      // Scan for inline R prefixes until the text is exhausted or indexed
87
      // beyond the caret position.
88
      while( index != INDEX_NOT_FOUND && index < offset ) {
89
        // Set rPrefix to the index that might precede the caret. The + 1 is
90
        // to skip passed the leading backtick in the prefix (`r#).
91
        rPrefix = index + 1;
92
93
        // If there are no more R prefixes, exit the loop and look for a
94
        // suffix starting from the rPrefix position.
95
        index = text.indexOf( PREFIX, rPrefix );
96
      }
97
98
      // Scan from the character after the R prefix up to any R suffix.
99
      final int rSuffix = max( text.indexOf( SUFFIX, rPrefix ), rPrefix );
100
101
      // If the caret falls between the rPrefix and rSuffix, then change the
102
      // insertion point.
103
      final boolean between = isBetween( offset, rPrefix, rSuffix );
104
      
105
      // Insert the caret marker at the start of the R statement.
106
      if( between ) {
107
        offset = rPrefix - 1;
108
      }
109
    }
110
111
    return inject( text, offset );
112
  }
113
}
1114
A src/main/java/com/scrivenvar/processors/RVariableProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
33
/**
34
 * Converts the keys of the resolved map from default form to R form, then
35
 * performs a substitution on the text. The default R variable syntax is
36
 * <code>v$tree$leaf</code>.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
class RVariableProcessor extends DefaultVariableProcessor {
41
42
  public RVariableProcessor(
43
    final Processor<String> rp, final Map<String, String> map ) {
44
    super( rp, map );
45
  }
46
47
  /**
48
   * Returns the R-based version of the interpolated variable definitions.
49
   *
50
   * @return Variable names transmogrified from the default syntax to R syntax.
51
   */
52
  @Override
53
  protected Map<String, String> getDefinitions() {
54
    return toR( super.getDefinitions() );
55
  }
56
57
  /**
58
   * Converts the given map from regular variables to R variables.
59
   *
60
   * @param map Map of variable names to values.
61
   *
62
   * @return
63
   */
64
  private Map<String, String> toR( final Map<String, String> map ) {
65
    final Map<String, String> rMap = new HashMap<>( map.size() );
66
67
    for( final String key : map.keySet() ) {
68
      rMap.put( toRKey( key ), toRValue( map.get( key ) ) );
69
    }
70
71
    return rMap;
72
  }
73
74
  /**
75
   * Transforms a variable name from $tree.branch.leaf$ to v$tree$branch$leaf
76
   * form.
77
   *
78
   * @param key The variable name to transform, can be empty but not null.
79
   *
80
   * @return The transformed variable name.
81
   */
82
  private String toRKey( final String key ) {
83
    // Replace all the periods with dollar symbols.
84
    final StringBuilder sb = new StringBuilder( 'v' + key );
85
    final int length = sb.length();
86
87
    // Replace all periods with dollar symbols. Normally we'd check i >= 0,
88
    // but the prepended 'v' is always going to be a 'v', not a dot.
89
    for( int i = length - 1; i > 0; i-- ) {
90
      if( sb.charAt( i ) == '.' ) {
91
        sb.setCharAt( i, '$' );
92
      }
93
    }
94
95
    // The length is always at least 1 (the 'v'), so bounds aren't broken here.
96
    sb.setLength( length - 1 );
97
98
    return sb.toString();
99
  }
100
101
  private String toRValue( final String value ) {
102
    return '\'' + escape( value, '\'', "\\'" ) + '\'';
103
  }
104
105
  /**
106
   * TODO: Make generic method for replacing text.
107
   * 
108
   * @see CaretReplacementProcessor.replace
109
   *
110
   * @param haystack Search this string for the needle, must not be null.
111
   * @param needle The character to find in the haystack.
112
   * @param thread Replace the needle with this text, if the needle is found.
113
   *
114
   * @return The haystack with the all instances of needle replaced with thread.
115
   */
116
  private String escape(
117
    final String haystack, final char needle, final String thread ) {
118
    int end = haystack.indexOf( needle );
119
120
    if( end < 0 ) {
121
      return haystack;
122
    }
123
124
    final int length = haystack.length();
125
    int start = 0;
126
127
    // Replace up to 32 occurrences before the string reallocates its buffer.
128
    final StringBuilder sb = new StringBuilder( length + 32 );
129
130
    while( end >= 0 ) {
131
      sb.append( haystack.substring( start, end ) ).append( thread );
132
      start = end + 1;
133
      end = haystack.indexOf( needle, start );
134
    }
135
136
    return sb.append( haystack.substring( start ) ).toString();
137
  }
138
}
1139
A src/main/java/com/scrivenvar/processors/XMLCaretInsertionProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.ximpleware.VTDException;
31
import com.ximpleware.VTDGen;
32
import static com.ximpleware.VTDGen.TOKEN_CHARACTER_DATA;
33
import com.ximpleware.VTDNav;
34
import java.text.ParseException;
35
import javafx.beans.value.ObservableValue;
36
37
/**
38
 * Inserts a caret position indicator into the document.
39
 *
40
 * @author White Magic Software, Ltd.
41
 */
42
public class XMLCaretInsertionProcessor extends CaretInsertionProcessor {
43
44
  private VTDGen parser;
45
46
  /**
47
   * Constructs a processor capable of inserting a caret marker into XML.
48
   *
49
   * @param processor The next processor in the chain.
50
   * @param position The caret's current position in the text, cannot be null.
51
   */
52
  public XMLCaretInsertionProcessor(
53
    final Processor<String> processor,
54
    final ObservableValue<Integer> position ) {
55
    super( processor, position );
56
  }
57
58
  /**
59
   * Inserts a caret at a valid position within the XML document.
60
   *
61
   * @param text The string into which caret position marker text is inserted.
62
   *
63
   * @return The text with a caret position marker included, or the original
64
   * text if no insertion point could be found.
65
   */
66
  @Override
67
  public String processLink( final String text ) {
68
    final int caret = getCaretPosition();
69
    int insertOffset = -1;
70
71
    if( text.length() > 0 ) {
72
      try {
73
        final VTDNav vn = getNavigator( text );
74
        final int tokens = vn.getTokenCount();
75
76
        int currTokenIndex = 0;
77
        int prevTokenIndex = currTokenIndex;
78
        int currOffset = 0;
79
80
        // To find the insertion spot even faster, the algorithm could
81
        // use a binary search or interpolation search algorithm. This
82
        // would reduce the worst-case iterations to O(log n) from O(n).
83
        while( currTokenIndex < tokens ) {
84
          if( vn.getTokenType( currTokenIndex ) == TOKEN_CHARACTER_DATA ) {
85
            final int prevOffset = currOffset;
86
            currOffset = vn.getTokenOffset( currTokenIndex );
87
88
            if( currOffset > caret ) {
89
              final int prevLength = vn.getTokenLength( prevTokenIndex );
90
91
              // If the caret falls within the limits of the previous token,
92
              // theninsert the caret position marker at the caret offset.
93
              if( isBetween( caret, prevOffset, prevOffset + prevLength ) ) {
94
                insertOffset = caret;
95
              } else {
96
                // The caret position is outside the previous token's text
97
                // boundaries, but not inside the current text token. The
98
                // caret should be positioned into the closer text token.
99
                // For now, the cursor is positioned at the start of the
100
                // current text token.
101
                insertOffset = currOffset;
102
              }
103
104
              break;
105
            }
106
107
            prevTokenIndex = currTokenIndex;
108
          }
109
110
          currTokenIndex++;
111
        }
112
113
      } catch( final Exception ex ) {
114
        throw new RuntimeException(
115
          new ParseException( ex.getMessage(), caret )
116
        );
117
      }
118
    }
119
120
    return inject( text, insertOffset );
121
  }
122
123
  /**
124
   * Parses the given XML document and returns a high-performance navigator
125
   * instance for scanning through the XML elements.
126
   *
127
   * @param xml The XML document to parse.
128
   *
129
   * @return A document navigator instance.
130
   */
131
  private VTDNav getNavigator( final String xml ) throws VTDException {
132
    final VTDGen vg = getParser();
133
134
    // TODO: Use the document's encoding...
135
    vg.setDoc( xml.getBytes() );
136
    vg.parse( true );
137
    return vg.getNav();
138
  }
139
140
  private synchronized VTDGen getParser() {
141
    if( this.parser == null ) {
142
      this.parser = createParser();
143
    }
144
145
    return this.parser;
146
  }
147
148
  /**
149
   * Creates a high-performance XML document parser.
150
   *
151
   * @return A new XML parser.
152
   */
153
  protected VTDGen createParser() {
154
    return new VTDGen();
155
  }
156
}
1157
A src/main/java/com/scrivenvar/processors/XMLProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.service.Snitch;
32
import java.io.File;
33
import java.io.IOException;
34
import java.io.Reader;
35
import java.io.StringReader;
36
import java.io.StringWriter;
37
import java.nio.file.Path;
38
import java.nio.file.Paths;
39
import java.text.ParseException;
40
import javax.xml.stream.XMLEventReader;
41
import javax.xml.stream.XMLInputFactory;
42
import javax.xml.stream.XMLStreamException;
43
import javax.xml.stream.events.ProcessingInstruction;
44
import javax.xml.stream.events.XMLEvent;
45
import javax.xml.transform.ErrorListener;
46
import javax.xml.transform.Source;
47
import javax.xml.transform.Transformer;
48
import javax.xml.transform.TransformerConfigurationException;
49
import javax.xml.transform.TransformerException;
50
import javax.xml.transform.TransformerFactory;
51
import javax.xml.transform.stream.StreamResult;
52
import javax.xml.transform.stream.StreamSource;
53
import net.sf.saxon.TransformerFactoryImpl;
54
import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute;
55
56
/**
57
 * Transforms an XML document. The XML document must have a stylesheet specified
58
 * as part of its processing instructions, such as:
59
 *
60
 * <code>xml-stylesheet type="text/xsl" href="markdown.xsl"</code>
61
 *
62
 * The XSL must transform the XML document into Markdown, or another format
63
 * recognized by the next link on the chain.
64
 *
65
 * @author White Magic Software, Ltd.
66
 */
67
public class XMLProcessor extends AbstractProcessor<String>
68
  implements ErrorListener {
69
70
  private final Snitch snitch = Services.load( Snitch.class );
71
72
  private XMLInputFactory xmlInputFactory;
73
  private TransformerFactory transformerFactory;
74
  private Transformer transformer;
75
76
  private Path path;
77
78
  /**
79
   * Constructs an XML processor that can transform an XML document into another
80
   * format based on the XSL file specified as a processing instruction. The
81
   * path must point to the directory where the XSL file is found, which implies
82
   * that they must be in the same directory.
83
   *
84
   * @param processor Next link in the processing chain.
85
   * @param path The path to the XML file content to be processed.
86
   */
87
  public XMLProcessor( final Processor<String> processor, final Path path ) {
88
    super( processor );
89
    setPath( path );
90
  }
91
92
  /**
93
   * Transforms the given XML text into another form (typically Markdown).
94
   *
95
   * @param text The text to transform, can be empty, cannot be null.
96
   *
97
   * @return The transformed text, or empty if text is empty.
98
   */
99
  @Override
100
  public String processLink( final String text ) {
101
    try {
102
      return text.isEmpty() ? text : transform( text );
103
    } catch( final Exception ex ) {
104
      throw new RuntimeException( ex );
105
    }
106
  }
107
108
  /**
109
   * Performs an XSL transformation on the given XML text. The XML text must
110
   * have a processing instruction that points to the XSL template file to use
111
   * for the transformation.
112
   *
113
   * @param text The text to transform.
114
   *
115
   * @return The transformed text.
116
   */
117
  private String transform( final String text ) throws Exception {
118
    // Extract the XML stylesheet processing instruction.
119
    final String template = getXsltFilename( text );
120
    final Path xsl = getXslPath( template );
121
122
    try(
123
      final StringWriter output = new StringWriter( text.length() );
124
      final StringReader input = new StringReader( text ) ) {
125
126
      // Listen for external file modification events.
127
      getSnitch().listen( xsl );
128
129
      getTransformer( xsl ).transform(
130
        new StreamSource( input ),
131
        new StreamResult( output )
132
      );
133
134
      return output.toString();
135
    }
136
  }
137
138
  /**
139
   * Returns an XSL transformer ready to transform an XML document using the
140
   * XSLT file specified by the given path. If the path is already known then
141
   * this will return the associated transformer.
142
   *
143
   * @param xsl The path to an XSLT file.
144
   *
145
   * @return A transformer that will transform XML documents using the given
146
   * XSLT file.
147
   *
148
   * @throws TransformerConfigurationException Could not instantiate the
149
   * transformer.
150
   */
151
  private Transformer getTransformer( final Path xsl )
152
    throws TransformerConfigurationException, IOException {
153
    if( this.transformer == null ) {
154
      this.transformer = createTransformer( xsl );
155
    }
156
157
    return this.transformer;
158
  }
159
160
  /**
161
   * Creates a configured transformer ready to run.
162
   *
163
   * @param xsl The stylesheet to use for transforming XML documents.
164
   *
165
   * @return The edited XML document transformed into another format (usually
166
   * markdown).
167
   *
168
   * @throws TransformerConfigurationException Could not create the transformer.
169
   */
170
  protected Transformer createTransformer( final Path xsl )
171
    throws TransformerConfigurationException {
172
    final Source xslt = new StreamSource( xsl.toFile() );
173
174
    return getTransformerFactory().newTransformer( xslt );
175
  }
176
177
  private Path getXslPath( final String filename ) {
178
    final Path xmlPath = getPath();
179
    final File xmlDirectory = xmlPath.toFile().getParentFile();
180
181
    return Paths.get( xmlDirectory.getPath(), filename );
182
  }
183
184
  /**
185
   * Given XML text, this will use a StAX pull reader to obtain the XML
186
   * stylesheet processing instruction. This will throw a parse exception if the
187
   * href pseudo-attribute filename value cannot be found.
188
   *
189
   * @param xml The XML containing an xml-stylesheet processing instruction.
190
   *
191
   * @return The href pseudo-attribute value.
192
   *
193
   * @throws XMLStreamException Could not parse the XML file.
194
   * @throws ParseException Could not find a non-empty HREF attribute value.
195
   */
196
  private String getXsltFilename( final String xml )
197
    throws XMLStreamException, ParseException {
198
199
    String result = "";
200
201
    try( final StringReader sr = new StringReader( xml ) ) {
202
      boolean found = false;
203
      int count = 0;
204
      final XMLEventReader reader = createXMLEventReader( sr );
205
206
      // If the processing instruction wasn't found in the first 10 lines,
207
      // fail fast. This should iterate twice through the loop.
208
      while( !found && reader.hasNext() && count++ < 10 ) {
209
        final XMLEvent event = reader.nextEvent();
210
211
        if( event.isProcessingInstruction() ) {
212
          final ProcessingInstruction pi = (ProcessingInstruction)event;
213
          final String target = pi.getTarget();
214
215
          if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
216
            result = getPseudoAttribute( pi.getData(), "href" );
217
            found = true;
218
          }
219
        }
220
      }
221
    }
222
223
    return result;
224
  }
225
226
  private XMLEventReader createXMLEventReader( final Reader reader )
227
    throws XMLStreamException {
228
    return getXMLInputFactory().createXMLEventReader( reader );
229
  }
230
231
  private synchronized XMLInputFactory getXMLInputFactory() {
232
    if( this.xmlInputFactory == null ) {
233
      this.xmlInputFactory = createXMLInputFactory();
234
    }
235
236
    return this.xmlInputFactory;
237
  }
238
239
  private XMLInputFactory createXMLInputFactory() {
240
    return XMLInputFactory.newInstance();
241
  }
242
243
  private synchronized TransformerFactory getTransformerFactory() {
244
    if( this.transformerFactory == null ) {
245
      this.transformerFactory = createTransformerFactory();
246
    }
247
248
    return this.transformerFactory;
249
  }
250
251
  /**
252
   * Returns a high-performance XSLT 2 transformation engine.
253
   *
254
   * @return An XSL transforming engine.
255
   */
256
  private TransformerFactory createTransformerFactory() {
257
    final TransformerFactory factory = new TransformerFactoryImpl();
258
259
    // Bubble problems up to the user interface, rather than standard error.
260
    factory.setErrorListener( this );
261
262
    return factory;
263
  }
264
265
  /**
266
   * Called when the XSL transformer issues a warning.
267
   *
268
   * @param ex The problem the transformer encountered.
269
   */
270
  @Override
271
  public void warning( final TransformerException ex ) {
272
    throw new RuntimeException( ex );
273
  }
274
275
  /**
276
   * Called when the XSL transformer issues an error.
277
   *
278
   * @param ex The problem the transformer encountered.
279
   */
280
  @Override
281
  public void error( final TransformerException ex ) {
282
    throw new RuntimeException( ex );
283
  }
284
285
  /**
286
   * Called when the XSL transformer issues a fatal error, which is probably
287
   * a bit over-dramatic a method name.
288
   *
289
   * @param ex The problem the transformer encountered.
290
   */
291
  @Override
292
  public void fatalError( final TransformerException ex ) {
293
    throw new RuntimeException( ex );
294
  }
295
296
  private void setPath( final Path path ) {
297
    this.path = path;
298
  }
299
300
  private Path getPath() {
301
    return this.path;
302
  }
303
304
  private Snitch getSnitch() {
305
    return this.snitch;
306
  }
307
}
1308
A src/main/java/com/scrivenvar/processors/text/AbstractTextReplacer.java
1
/*
2
 * The MIT License
3
 *
4
 * Copyright 2016 .
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy
7
 * of this software and associated documentation files (the "Software"), to deal
8
 * in the Software without restriction, including without limitation the rights
9
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
 * copies of the Software, and to permit persons to whom the Software is
11
 * furnished to do so, subject to the following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be included in
14
 * all copies or substantial portions of the Software.
15
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
 * THE SOFTWARE.
23
 */
24
package com.scrivenvar.processors.text;
25
26
import java.util.Map;
27
28
/**
29
 * Responsible for common behaviour across all text replacer implementations.
30
 *
31
 * @author White Magic Software, Ltd.
32
 */
33
public abstract class AbstractTextReplacer implements TextReplacer {
34
35
  /**
36
   * Default (empty) constructor.
37
   */
38
  protected AbstractTextReplacer() {
39
  }
40
41
  protected String[] keys( final Map<String, String> map ) {
42
    return map.keySet().toArray( new String[ map.size() ] );
43
  }
44
45
  protected String[] values( final Map<String, String> map ) {
46
    return map.values().toArray( new String[ map.size() ] );
47
  }
48
}
149
A src/main/java/com/scrivenvar/processors/text/AhoCorasickReplacer.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
import org.ahocorasick.trie.Emit;
32
import org.ahocorasick.trie.Trie.TrieBuilder;
33
import static org.ahocorasick.trie.Trie.builder;
34
35
/**
36
 * Replaces text using an Aho-Corasick algorithm.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class AhoCorasickReplacer extends AbstractTextReplacer {
41
42
  /**
43
   * Default (empty) constructor.
44
   */
45
  protected AhoCorasickReplacer() {
46
  }
47
48
  @Override
49
  public String replace( final String text, final Map<String, String> map ) {
50
    // Create a buffer sufficiently large that re-allocations are minimized.
51
    final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) );
52
53
    // The TrieBuilder should only match whole words and ignore overlaps (there
54
    // shouldn't be any).
55
    final TrieBuilder builder = builder().onlyWholeWords().removeOverlaps();
56
57
    for( final String key : keys( map ) ) {
58
      builder.addKeyword( key );
59
    }
60
61
    int index = 0;
62
63
    // Replace all instances with dereferenced variables.
64
    for( final Emit emit : builder.build().parseText( text ) ) {
65
      sb.append( text.substring( index, emit.getStart() ) );
66
      sb.append( map.get( emit.getKeyword() ) );
67
      index = emit.getEnd() + 1;
68
    }
69
70
    // Add the remainder of the string (contains no more matches).
71
    sb.append( text.substring( index ) );
72
73
    return sb.toString();
74
  }
75
}
176
A src/main/java/com/scrivenvar/processors/text/StringUtilsReplacer.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
import static org.apache.commons.lang3.StringUtils.replaceEach;
32
33
/**
34
 * Replaces text using Apache's StringUtils.replaceEach method.
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public class StringUtilsReplacer extends AbstractTextReplacer {
39
40
  /**
41
   * Default (empty) constructor.
42
   */
43
  protected StringUtilsReplacer() {
44
  }
45
46
  @Override
47
  public String replace( final String text, final Map<String, String> map ) {
48
    return replaceEach( text, keys( map ), values( map ) );
49
  }
50
}
151
A src/main/java/com/scrivenvar/processors/text/TextReplacementFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Used to generate a class capable of efficiently replacing variable
34
 * definitions with their values.
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public final class TextReplacementFactory {
39
40
  private final static TextReplacer APACHE = new StringUtilsReplacer();
41
  private final static TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
42
43
  /**
44
   * Returns a text search/replacement instance that is reasonably optimal for
45
   * the given length of text.
46
   *
47
   * @param length The length of text that requires some search and replacing.
48
   *
49
   * @return A class that can search and replace text with utmost expediency.
50
   */
51
  public static TextReplacer getTextReplacer( final int length ) {
52
    // After about 1,500 characters, the StringUtils implementation is less
53
    // performant than the Aho-Corsick implementation.
54
    //
55
    // Ssee http://stackoverflow.com/a/40836618/59087
56
    return length < 1500 ? APACHE : AHO_CORASICK;
57
  }
58
59
  /**
60
   * Convenience method to instantiate a suitable text replacer algorithm and
61
   * perform a replacement using the given map. At this point, the values should
62
   * be already dereferenced and ready to be substituted verbatim; any
63
   * recursively defined values must have been interpolated previously.
64
   *
65
   * @param text The text containing zero or more variables to replace.
66
   * @param map The map of variables to their dereferenced values.
67
   *
68
   * @return The text with all variables replaced.
69
   */
70
  public static String replace(
71
    final String text, final Map<String, String> map ) {
72
    return getTextReplacer( text.length() ).replace( text, map );
73
  }
74
}
175
A src/main/java/com/scrivenvar/processors/text/TextReplacer.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Defines the ability to replace text given a set of keys and values.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public interface TextReplacer {
38
39
  /**
40
   * Searches through the given text for any of the keys given in the map and
41
   * replaces the keys that appear in the text with the key's corresponding
42
   * value.
43
   *
44
   * @param text The text that contains zero or more keys.
45
   * @param map The set of keys mapped to replacement values.
46
   *
47
   * @return The given text with all keys replaced with corresponding values.
48
   */
49
  public String replace( String text, Map<String, String> map );
50
}
151
A src/main/java/com/scrivenvar/service/Options.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import java.util.prefs.BackingStoreException;
31
import java.util.prefs.Preferences;
32
33
/**
34
 * Responsible for persistent options.
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public interface Options extends Service {
39
40
  public Preferences getState();
41
42
  /**
43
   * Stores the key and value into the user preferences to be loaded the next
44
   * time the application is launched.
45
   *
46
   * @param key Name of the key to persist along with its value.
47
   * @param value Value to associate with the key.
48
   *
49
   * @throws BackingStoreException Could not persist the change.
50
   */
51
  public void put( String key, String value ) throws BackingStoreException;
52
53
  /**
54
   * Retrieves the value for a key in the user preferences.
55
   *
56
   * @param key Retrieve the value of this key.
57
   * @param defaultValue The value to return in the event that the given key has
58
   * no associated value.
59
   *
60
   * @return The value associated with the key.
61
   */
62
  public String get( String key, String defaultValue );
63
  
64
  /**
65
   * Retrieves the value for a key in the user preferences. This will return
66
   * the empty string if the value cannot be found.
67
   * 
68
   * @param key The key to find in the preferences.
69
   * @return A non-null, possibly empty value for the key.
70
   */
71
  public String get( String key );
72
}
173
A src/main/java/com/scrivenvar/service/Service.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
/**
31
 * All services inherit from this one.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public interface Service {
36
}
137
A src/main/java/com/scrivenvar/service/Settings.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import java.util.Iterator;
31
import java.util.List;
32
33
/**
34
 * Defines how settings and options can be retrieved.
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public interface Settings extends Service {
39
40
  /**
41
   * Returns a setting property or its default value.
42
   *
43
   * @param property The property key name to obtain its value.
44
   * @param defaultValue The default value to return iff the property cannot be
45
   * found.
46
   *
47
   * @return The property value for the given property key.
48
   */
49
  public String getSetting( String property, String defaultValue );
50
51
  /**
52
   * Returns a setting property or its default value.
53
   *
54
   * @param property The property key name to obtain its value.
55
   * @param defaultValue The default value to return iff the property cannot be
56
   * found.
57
   *
58
   * @return The property value for the given property key.
59
   */
60
  public int getSetting( String property, int defaultValue );
61
62
  /**
63
   * Returns a list of property names that begin with the given prefix. The
64
   * prefix is included in any matching results. This will return keys that
65
   * either match the prefix or start with the prefix followed by a dot ('.').
66
   * For example, a prefix value of <code>the.property.name</code> will likely
67
   * return the expected results, but <code>the.property.name.</code> (note the
68
   * extraneous period) will probably not.
69
   *
70
   * @param prefix The prefix to compare against each property name.
71
   *
72
   * @return The list of property names that have the given prefix.
73
   */
74
  public Iterator<String> getKeys( final String prefix );
75
76
  /**
77
   * Convert the generic list of property objects into strings.
78
   *
79
   * @param property The property value to coerce.
80
   * @param defaults The defaults values to use should the property be unset.
81
   *
82
   * @return The list of properties coerced from objects to strings.
83
   */
84
  public List<String> getStringSettingList( String property, List<String> defaults );
85
86
  /**
87
   * Converts the generic list of property objects into strings.
88
   *
89
   * @param property The property value to coerce.
90
   *
91
   * @return The list of properties coerced from objects to strings.
92
   */
93
  public List<String> getStringSettingList( String property );
94
95
96
  /**
97
   * Changes key's value. This will clear the old value before setting the
98
   * new value so that the old value is erased, not changed into a list.
99
   *
100
   * @param key The property key name to obtain its value.
101
   * @param value The new value to set.
102
   */
103
  public void putSetting( String key, String value );
104
}
1105
A src/main/java/com/scrivenvar/service/Snitch.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import java.io.IOException;
31
import java.nio.file.Path;
32
import java.util.Observer;
33
34
/**
35
 * Listens for changes to file system files and directories.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public interface Snitch extends Service, Runnable {
40
41
  /**
42
   * Adds an observer to the set of observers for this object, provided that it
43
   * is not the same as some observer already in the set. The order in which
44
   * notifications will be delivered to multiple observers is not specified.
45
   *
46
   * @param o The object to receive changed events for when monitored files
47
   * are changed.
48
   */
49
  public void addObserver( Observer o );
50
51
  /**
52
   * Listens for changes to the path. If the path specifies a file, then only
53
   * notifications pertaining to that file are sent. Otherwise, change events
54
   * for the directory that contains the file are sent. This method must allow
55
   * for multiple calls to the same file without incurring additional listeners
56
   * or events.
57
   *
58
   * @param file Send notifications when this file changes, can be null.
59
   *
60
   * @throws IOException Couldn't create a watcher for the given file.
61
   */
62
  public void listen( Path file ) throws IOException;
63
64
  /**
65
   * Removes the given file from the notifications list.
66
   *
67
   * @param file The file to stop monitoring for any changes, can be null.
68
   */
69
  public void ignore( Path file );
70
71
  /**
72
   * Stop listening for events.
73
   */
74
  public void stop();
75
}
176
A src/main/java/com/scrivenvar/service/events/Notification.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
/**
31
 * Represents a message that contains a title and content.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public interface Notification {
36
37
  /**
38
   * Alert title.
39
   *
40
   * @return A non-null string to use as alert message title.
41
   */
42
  public String getTitle();
43
44
  /**
45
   * Alert message content.
46
   *
47
   * @return A non-null string that contains information for the user.
48
   */
49
  public String getContent();
50
}
151
A src/main/java/com/scrivenvar/service/events/Notifier.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
import java.util.Observer;
31
import javafx.scene.control.Alert;
32
import javafx.scene.control.ButtonType;
33
import javafx.stage.Window;
34
35
/**
36
 * Provides the application with a uniform way to notify the user of events.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public interface Notifier {
41
42
  public static final ButtonType YES = ButtonType.YES;
43
  public static final ButtonType NO = ButtonType.NO;
44
  public static final ButtonType CANCEL = ButtonType.CANCEL;
45
46
  /**
47
   * Notifies the user of a problem.
48
   *
49
   * @param message The problem description.
50
   */
51
  public void notify( final String message );
52
53
  /**
54
   * Notifies the user about the exception.
55
   *
56
   * @param ex The exception containing a message to show to the user.
57
   */
58
  default public void notify( final Exception ex ) {
59
    notify( ex.getMessage() );
60
  }
61
62
  /**
63
   * Causes any displayed notifications to disappear.
64
   */
65
  public void clear();
66
67
  /**
68
   * Constructs a default alert message text for a modal alert dialog.
69
   *
70
   * @param title The dialog box message title.
71
   * @param message The dialog box message content (needs formatting).
72
   * @param args The arguments to the message content that must be formatted.
73
   *
74
   * @return The message suitable for building a modal alert dialog.
75
   */
76
  public Notification createNotification(
77
    String title,
78
    String message,
79
    Object... args );
80
81
  /**
82
   * Creates an alert of alert type error with a message showing the cause of
83
   * the error.
84
   *
85
   * @param parent Dialog box owner (for modal purposes).
86
   * @param message The error message, title, and possibly more details.
87
   *
88
   * @return A modal alert dialog box ready to display using showAndWait.
89
   */
90
  public Alert createError( Window parent, Notification message );
91
92
  /**
93
   * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
94
   *
95
   * @param parent Dialog box owner (for modal purposes).
96
   * @param message The message, title, and possibly more details.
97
   *
98
   * @return A modal alert dialog box ready to display using showAndWait.
99
   */
100
  public Alert createConfirmation( Window parent, Notification message );
101
102
  /**
103
   * Adds an observer to the list of objects that receive notifications about
104
   * error messages to be presented to the user.
105
   *
106
   * @param observer The observer instance to notify.
107
   */
108
  public void addObserver( Observer observer );
109
110
  /**
111
   * Removes an observer from the list of objects that receive notifications
112
   * about error messages to be presented to the user.
113
   *
114
   * @param observer The observer instance to no longer notify.
115
   */
116
  public void deleteObserver( Observer observer );
117
}
1118
A src/main/java/com/scrivenvar/service/events/impl/ButtonOrderPane.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.service.Settings;
32
import javafx.scene.Node;
33
import javafx.scene.control.ButtonBar;
34
import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS;
35
import javafx.scene.control.DialogPane;
36
37
/**
38
 * Ensures a consistent button order for alert dialogs across platforms (because
39
 * the default button order on Linux defies all logic).
40
 *
41
 * @author White Magic Software, Ltd.
42
 */
43
public class ButtonOrderPane extends DialogPane {
44
45
  @Override
46
  protected Node createButtonBar() {
47
    final ButtonBar node = (ButtonBar)super.createButtonBar();
48
    node.setButtonOrder( getButtonOrder() );
49
    return node;
50
  }
51
52
  private String getButtonOrder() {
53
    return getSetting( "dialog.alert.button.order.windows", BUTTON_ORDER_WINDOWS );
54
  }
55
56
  private String getSetting( final String key, final String defaultValue ) {
57
    return getSettings().getSetting( key, defaultValue );
58
  }
59
60
  private Settings getSettings() {
61
    return Services.load( Settings.class );
62
  }
63
}
164
A src/main/java/com/scrivenvar/service/events/impl/DefaultNotification.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.service.events.Notification;
31
import java.text.MessageFormat;
32
33
/**
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public class DefaultNotification implements Notification {
38
39
  private final String title;
40
  private final String content;
41
42
  /**
43
   * Constructs default message text for a notification.
44
   * 
45
   * @param title The message title.
46
   * @param message The message content (needs formatting).
47
   * @param args The arguments to the message content that must be formatted.
48
   */
49
  public DefaultNotification(
50
    final String title,
51
    final String message,
52
    final Object... args ) {
53
    this.title = title;
54
    this.content = MessageFormat.format( message, args );
55
  }
56
57
  @Override
58
  public String getTitle() {
59
    return this.title;
60
  }
61
62
  @Override
63
  public String getContent() {
64
    return this.content;
65
  }
66
}
167
A src/main/java/com/scrivenvar/service/events/impl/DefaultNotifier.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import static com.scrivenvar.Constants.STATUS_BAR_DEFAULT;
31
import com.scrivenvar.service.events.Notification;
32
import com.scrivenvar.service.events.Notifier;
33
import java.util.Observable;
34
import javafx.scene.control.Alert;
35
import javafx.scene.control.Alert.AlertType;
36
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
37
import static javafx.scene.control.Alert.AlertType.ERROR;
38
import javafx.stage.Window;
39
40
/**
41
 * Provides the ability to notify the user of problems.
42
 *
43
 * @author White Magic Software, Ltd.
44
 */
45
public final class DefaultNotifier extends Observable implements Notifier {
46
47
  public DefaultNotifier() {
48
  }
49
50
  /**
51
   * Notifies all observer instances of the given message.
52
   *
53
   * @param message The text to display to the user.
54
   */
55
  @Override
56
  public void notify( final String message ) {
57
    setChanged();
58
    notifyObservers( message );
59
  }
60
61
  @Override
62
  public void clear() {
63
    notify( STATUS_BAR_DEFAULT );
64
  }
65
66
  /**
67
   * Contains all the information that the user needs to know about a problem.
68
   *
69
   * @param title The context for the message.
70
   * @param message The message content (formatted with the given args).
71
   * @param args Parameters for the message content.
72
   *
73
   * @return A notification instance, never null.
74
   */
75
  @Override
76
  public Notification createNotification(
77
    final String title,
78
    final String message,
79
    final Object... args ) {
80
    return new DefaultNotification( title, message, args );
81
  }
82
83
  private Alert createAlertDialog(
84
    final Window parent,
85
    final AlertType alertType,
86
    final Notification message ) {
87
88
    final Alert alert = new Alert( alertType );
89
90
    alert.setDialogPane( new ButtonOrderPane() );
91
    alert.setTitle( message.getTitle() );
92
    alert.setHeaderText( null );
93
    alert.setContentText( message.getContent() );
94
    alert.initOwner( parent );
95
96
    return alert;
97
  }
98
99
  @Override
100
  public Alert createConfirmation( final Window parent, final Notification message ) {
101
    final Alert alert = createAlertDialog( parent, CONFIRMATION, message );
102
103
    alert.getButtonTypes().setAll( YES, NO, CANCEL );
104
105
    return alert;
106
  }
107
108
  @Override
109
  public Alert createError( final Window parent, final Notification message ) {
110
    return createAlertDialog( parent, ERROR, message );
111
  }
112
}
1113
A src/main/java/com/scrivenvar/service/events/impl/FileType.java
1
/*
2
 * To change this license header, choose License Headers in Project Properties.
3
 * To change this template file, choose Tools | Templates
4
 * and open the template in the editor.
5
 */
6
package com.scrivenvar.service.events.impl;
7
8
/**
9
 * Lists known file types for creating document processors via the factory.
10
 *
11
 * @author White Magic Software, Ltd.
12
 */
13
public enum FileType {
14
  MARKDOWN("md", "markdown", "mkdown", "mdown", "mkdn", "mkd", "mdwn", "mdtxt", "mdtext", "text", "txt"),
15
  R_MARKDOWN("Rmd"),
16
  XML("xml");
17
18
  private final String[] extensions;
19
20
  private FileType(final String... extensions) {
21
    this.extensions = extensions;
22
  }
23
24
  /**
25
   * Returns true if the given file type aligns with the extension for this
26
   * enumeration.
27
   *
28
   * @param filetype The file extension to compare against the internal list.
29
   * @return true The given filetype equals (case insensitive) the internal
30
   * type.
31
   */
32
  public boolean isType(final String filetype) {
33
    boolean result = false;
34
35
    for (final String extension : this.extensions) {
36
      if (extension.equalsIgnoreCase(filetype)) {
37
        result = true;
38
        break;
39
      }
40
    }
41
42
    return result;
43
  }
44
}
145
A src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.service.impl;
28
29
import static com.scrivenvar.Constants.PREFS_OPTIONS;
30
import static com.scrivenvar.Constants.PREFS_ROOT;
31
import static com.scrivenvar.Constants.PREFS_STATE;
32
import com.scrivenvar.service.Options;
33
import java.util.prefs.BackingStoreException;
34
import java.util.prefs.Preferences;
35
import static java.util.prefs.Preferences.userRoot;
36
37
/**
38
 * Persistent options user can change at runtime.
39
 *
40
 * @author Karl Tauber and White Magic Software, Ltd.
41
 */
42
public class DefaultOptions implements Options {
43
44
  private Preferences preferences;
45
46
  public DefaultOptions() {
47
    setPreferences( getRootPreferences().node( PREFS_OPTIONS ) );
48
  }
49
50
  /**
51
   * This will throw IllegalArgumentException if the value exceeds the maximum
52
   * preferences value length.
53
   *
54
   * @param key The name of the key to associate with the value.
55
   * @param value The value to persist.
56
   *
57
   * @throws BackingStoreException New value not persisted.
58
   */
59
  @Override
60
  public void put( final String key, final String value )
61
    throws BackingStoreException {
62
    getState().put( key, value );
63
    getState().flush();
64
  }
65
66
  @Override
67
  public String get( final String key, final String value ) {
68
    return getState().get( key, value );
69
  }
70
71
  @Override
72
  public String get( final String key ) {
73
    return get( key, "" );
74
  }
75
76
  private void setPreferences( final Preferences preferences ) {
77
    this.preferences = preferences;
78
  }
79
80
  private Preferences getRootPreferences() {
81
    return userRoot().node( PREFS_ROOT );
82
  }
83
84
  @Override
85
  public Preferences getState() {
86
    return getRootPreferences().node( PREFS_STATE );
87
  }
88
89
  private Preferences getPreferences() {
90
    return this.preferences;
91
  }
92
}
193
A src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.impl;
29
30
import static com.scrivenvar.Constants.SETTINGS_NAME;
31
import com.scrivenvar.service.Settings;
32
import java.io.IOException;
33
import java.io.InputStreamReader;
34
import java.io.Reader;
35
import java.net.URISyntaxException;
36
import java.net.URL;
37
import java.nio.charset.Charset;
38
import java.util.Iterator;
39
import java.util.List;
40
import org.apache.commons.configuration2.PropertiesConfiguration;
41
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
42
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
43
import org.apache.commons.configuration2.ex.ConfigurationException;
44
45
/**
46
 * Responsible for loading settings that help avoid hard-coded assumptions.
47
 *
48
 * @author White Magic Software, Ltd.
49
 */
50
public class DefaultSettings implements Settings {
51
52
  private static final char VALUE_SEPARATOR = ',';
53
54
  private PropertiesConfiguration properties;
55
56
  public DefaultSettings()
57
    throws ConfigurationException, URISyntaxException, IOException {
58
    setProperties( createProperties() );
59
  }
60
61
  /**
62
   * Returns the value of a string property.
63
   *
64
   * @param property The property key.
65
   * @param defaultValue The value to return if no property key has been set.
66
   *
67
   * @return The property key value, or defaultValue when no key found.
68
   */
69
  @Override
70
  public String getSetting( final String property, final String defaultValue ) {
71
    return getSettings().getString( property, defaultValue );
72
  }
73
74
  /**
75
   * Returns the value of a string property.
76
   *
77
   * @param property The property key.
78
   * @param defaultValue The value to return if no property key has been set.
79
   *
80
   * @return The property key value, or defaultValue when no key found.
81
   */
82
  @Override
83
  public int getSetting( final String property, final int defaultValue ) {
84
    return getSettings().getInt( property, defaultValue );
85
  }
86
87
  /**
88
   * Changes key's value. This will clear the old value before setting the new
89
   * value so that the old value is erased, not changed into a list.
90
   *
91
   * @param key The property key name to obtain its value.
92
   * @param value The new value to set.
93
   */
94
  @Override
95
  public void putSetting( final String key, final String value ) {
96
    getSettings().clearProperty( key );
97
    getSettings().addProperty( key, value );
98
  }
99
100
  /**
101
   * Convert the generic list of property objects into strings.
102
   *
103
   * @param property The property value to coerce.
104
   * @param defaults The defaults values to use should the property be unset.
105
   *
106
   * @return The list of properties coerced from objects to strings.
107
   */
108
  @Override
109
  public List<String> getStringSettingList(
110
    final String property, final List<String> defaults ) {
111
    return getSettings().getList( String.class, property, defaults );
112
  }
113
114
  /**
115
   * Convert a list of property objects into strings, with no default value.
116
   *
117
   * @param property The property value to coerce.
118
   *
119
   * @return The list of properties coerced from objects to strings.
120
   */
121
  @Override
122
  public List<String> getStringSettingList( final String property ) {
123
    return getStringSettingList( property, null );
124
  }
125
126
  /**
127
   * Returns a list of property names that begin with the given prefix.
128
   *
129
   * @param prefix The prefix to compare against each property name.
130
   *
131
   * @return The list of property names that have the given prefix.
132
   */
133
  @Override
134
  public Iterator<String> getKeys( final String prefix ) {
135
    return getSettings().getKeys( prefix );
136
  }
137
138
  private PropertiesConfiguration createProperties()
139
    throws ConfigurationException {
140
141
    final URL url = getPropertySource();
142
    final PropertiesConfiguration configuration = new PropertiesConfiguration();
143
144
    if( url != null ) {
145
      try( final Reader r = new InputStreamReader( url.openStream(), getDefaultEncoding() ) ) {
146
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
147
        configuration.read( r );
148
149
      } catch( final IOException ex ) {
150
        throw new RuntimeException( new ConfigurationException( ex ) );
151
      }
152
    }
153
154
    return configuration;
155
  }
156
157
  protected Charset getDefaultEncoding() {
158
    return Charset.defaultCharset();
159
  }
160
161
  protected ListDelimiterHandler createListDelimiterHandler() {
162
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
163
  }
164
165
  private URL getPropertySource() {
166
    return DefaultSettings.class.getResource( getSettingsFilename() );
167
  }
168
169
  private String getSettingsFilename() {
170
    return SETTINGS_NAME;
171
  }
172
173
  private void setProperties( final PropertiesConfiguration configuration ) {
174
    this.properties = configuration;
175
  }
176
177
  private PropertiesConfiguration getSettings() {
178
    return this.properties;
179
  }
180
}
1181
A src/main/java/com/scrivenvar/service/impl/DefaultSnitch.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.impl;
29
30
import static com.scrivenvar.Constants.APP_WATCHDOG_TIMEOUT;
31
import com.scrivenvar.service.Snitch;
32
import java.io.IOException;
33
import java.nio.file.FileSystem;
34
import java.nio.file.FileSystems;
35
import java.nio.file.Files;
36
import java.nio.file.Path;
37
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
38
import java.nio.file.WatchEvent;
39
import java.nio.file.WatchKey;
40
import java.nio.file.WatchService;
41
import java.util.Collections;
42
import java.util.Map;
43
import java.util.Observable;
44
import java.util.Set;
45
import java.util.concurrent.ConcurrentHashMap;
46
47
/**
48
 * Listens for file changes. Other classes can register paths to be monitored
49
 * and listen for changes to those paths.
50
 *
51
 * @author White Magic Software, Ltd.
52
 */
53
public class DefaultSnitch extends Observable implements Snitch {
54
55
  /**
56
   * Service for listening to directories for modifications.
57
   */
58
  private WatchService watchService;
59
60
  /**
61
   * Directories being monitored for changes.
62
   */
63
  private Map<WatchKey, Path> keys;
64
65
  /**
66
   * Files that will kick off notification events if modified.
67
   */
68
  private Set<Path> eavesdropped;
69
70
  /**
71
   * Set to true when running; set to false to stop listening.
72
   */
73
  private volatile boolean listening;
74
75
  public DefaultSnitch() {
76
  }
77
78
  @Override
79
  public void stop() {
80
    setListening( false );
81
  }
82
83
  /**
84
   * Adds a listener to the list of files to watch for changes. If the file is
85
   * already in the monitored list, this will return immediately.
86
   *
87
   * @param file Path to a file to watch for changes.
88
   *
89
   * @throws IOException The file could not be monitored.
90
   */
91
  @Override
92
  public void listen( final Path file ) throws IOException {
93
    if( file != null && getEavesdropped().add( file ) ) {
94
      final Path dir = toDirectory( file );
95
      final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY );
96
97
      getWatchMap().put( key, dir );
98
    }
99
  }
100
101
  /**
102
   * Returns the given path to a file (or directory) as a directory. If the
103
   * given path is already a directory, it is returned. Otherwise, this returns
104
   * the directory that contains the file. This will fail if the file is stored
105
   * in the root folder.
106
   *
107
   * @param path The file to return as a directory, which should always be the
108
   * case.
109
   *
110
   * @return The given path as a directory, if a file, otherwise the path
111
   * itself.
112
   */
113
  private Path toDirectory( final Path path ) {
114
    return Files.isDirectory( path )
115
      ? path
116
      : path.toFile().getParentFile().toPath();
117
  }
118
119
  /**
120
   * Stop listening to the given file for change events. This fails silently.
121
   *
122
   * @param file The file to no longer monitor for changes.
123
   */
124
  @Override
125
  public void ignore( final Path file ) {
126
    if( file != null ) {
127
      final Path directory = toDirectory( file );
128
129
      // Remove all occurrences (there should be only one).
130
      getWatchMap().values().removeAll( Collections.singleton( directory ) );
131
132
      // Remove all occurrences (there can be only one).
133
      getEavesdropped().remove( file );
134
    }
135
  }
136
137
  /**
138
   * Loops until stop is called, or the application is terminated.
139
   */
140
  @Override
141
  @SuppressWarnings( "SleepWhileInLoop" )
142
  public void run() {
143
    setListening( true );
144
145
    while( isListening() ) {
146
      try {
147
        final WatchKey key = getWatchService().take();
148
        final Path path = get( key );
149
150
        // Prevent receiving two separate ENTRY_MODIFY events: file modified
151
        // and timestamp updated. Instead, receive one ENTRY_MODIFY event
152
        // with two counts.
153
        Thread.sleep( APP_WATCHDOG_TIMEOUT );
154
155
        for( final WatchEvent<?> event : key.pollEvents() ) {
156
          final Path changed = path.resolve( (Path)event.context() );
157
158
          if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
159
            setChanged();
160
            notifyObservers( changed );
161
          }
162
        }
163
164
        if( !key.reset() ) {
165
          ignore( path );
166
        }
167
      } catch( final IOException | InterruptedException ex ) {
168
        // Stop eavesdropping.
169
        setListening( false );
170
      }
171
    }
172
  }
173
174
  /**
175
   * Returns true if the list of files being listened to for changes contains
176
   * the given file.
177
   *
178
   * @param file Path to a system file.
179
   *
180
   * @return true The given file is being monitored for changes.
181
   */
182
  private boolean isListening( final Path file ) {
183
    return getEavesdropped().contains( file );
184
  }
185
186
  /**
187
   * Returns a path for a given watch key.
188
   *
189
   * @param key The key to lookup its corresponding path.
190
   *
191
   * @return The path for the given key.
192
   */
193
  private Path get( final WatchKey key ) {
194
    return getWatchMap().get( key );
195
  }
196
197
  private synchronized Map<WatchKey, Path> getWatchMap() {
198
    if( this.keys == null ) {
199
      this.keys = createWatchKeys();
200
    }
201
202
    return this.keys;
203
  }
204
205
  protected Map<WatchKey, Path> createWatchKeys() {
206
    return new ConcurrentHashMap<>();
207
  }
208
209
  /**
210
   * Returns a list of files that, when changed, will kick off a notification.
211
   *
212
   * @return A non-null, possibly empty, list of files.
213
   */
214
  private synchronized Set<Path> getEavesdropped() {
215
    if( this.eavesdropped == null ) {
216
      this.eavesdropped = createEavesdropped();
217
    }
218
219
    return this.eavesdropped;
220
  }
221
222
  protected Set<Path> createEavesdropped() {
223
    return ConcurrentHashMap.newKeySet();
224
  }
225
226
  /**
227
   * The existing watch service, or a new instance if null.
228
   *
229
   * @return A valid WatchService instance, never null.
230
   *
231
   * @throws IOException Could not create a new watch service.
232
   */
233
  private synchronized WatchService getWatchService() throws IOException {
234
    if( this.watchService == null ) {
235
      this.watchService = createWatchService();
236
    }
237
238
    return this.watchService;
239
  }
240
241
  protected WatchService createWatchService() throws IOException {
242
    final FileSystem fileSystem = FileSystems.getDefault();
243
    return fileSystem.newWatchService();
244
  }
245
246
  /**
247
   * Answers whether the loop should continue executing.
248
   *
249
   * @return true The internal listening loop should continue listening for file
250
   * modification events.
251
   */
252
  protected boolean isListening() {
253
    return this.listening;
254
  }
255
256
  /**
257
   * Requests the snitch to stop eavesdropping on file changes.
258
   *
259
   * @param listening Use true to indicate the service should stop running.
260
   */
261
  private void setListening( final boolean listening ) {
262
    this.listening = listening;
263
  }
264
}
1265
A src/main/java/com/scrivenvar/test/TestProperties.java
1
package com.scrivenvar.test;
2
3
import java.io.IOException;
4
import java.io.StringReader;
5
import java.util.Arrays;
6
import org.apache.commons.configuration2.PropertiesConfiguration;
7
import org.apache.commons.configuration2.ex.ConfigurationException;
8
9
public class TestProperties {
10
11
  public static void main( final String args[] ) throws ConfigurationException, IOException {
12
    final String p = ""
13
      + "file.ext.definition.yaml=*.yml,*.yaml\n"
14
      + "filter.file.ext.definition=${file.ext.definition.yaml}\n";
15
16
    try( final StringReader r = new StringReader( p ) ) {
17
18
      PropertiesConfiguration config = new PropertiesConfiguration();
19
      config.read( r );
20
21
      System.out.println( config.getList( "filter.file.ext.definition" ) );
22
      System.out.println( config.getString( "filter.file.ext.definition" ) );
23
      System.out.println( Arrays.toString( config.getStringArray( "filter.file.ext.definition" ) ) );
24
    }
25
  }
26
}
127
A src/main/java/com/scrivenvar/util/Action.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import de.jensd.fx.glyphs.GlyphIcons;
30
import javafx.beans.value.ObservableBooleanValue;
31
import javafx.event.ActionEvent;
32
import javafx.event.EventHandler;
33
import javafx.scene.input.KeyCombination;
34
35
/**
36
 * Simple action class
37
 *
38
 * @author Karl Tauber
39
 */
40
public class Action {
41
42
  public final String text;
43
  public final KeyCombination accelerator;
44
  public final GlyphIcons icon;
45
  public final EventHandler<ActionEvent> action;
46
  public final ObservableBooleanValue disable;
47
48
  public Action(
49
    final String text,
50
    final String accelerator,
51
    final GlyphIcons icon,
52
    final EventHandler<ActionEvent> action ) {
53
    this( text, accelerator, icon, action, null );
54
  }
55
56
  public Action(
57
    final String text,
58
    final String accelerator,
59
    final GlyphIcons icon,
60
    final EventHandler<ActionEvent> action,
61
    final ObservableBooleanValue disable ) {
62
63
    this.text = text;
64
    this.accelerator = (accelerator != null)
65
      ? KeyCombination.valueOf( accelerator )
66
      : null;
67
    this.icon = icon;
68
    this.action = action;
69
    this.disable = disable;
70
  }
71
}
172
A src/main/java/com/scrivenvar/util/ActionUtils.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
30
import javafx.scene.Node;
31
import javafx.scene.control.Button;
32
import javafx.scene.control.Menu;
33
import javafx.scene.control.MenuItem;
34
import javafx.scene.control.Separator;
35
import javafx.scene.control.SeparatorMenuItem;
36
import javafx.scene.control.ToolBar;
37
import javafx.scene.control.Tooltip;
38
39
/**
40
 * Action utilities
41
 *
42
 * @author Karl Tauber
43
 */
44
public class ActionUtils {
45
46
  public static Menu createMenu( final String text, final Action... actions ) {
47
    return new Menu( text, null, createMenuItems( actions ) );
48
  }
49
50
  public static MenuItem[] createMenuItems( Action... actions ) {
51
    MenuItem[] menuItems = new MenuItem[ actions.length ];
52
    for( int i = 0; i < actions.length; i++ ) {
53
      menuItems[ i ] = (actions[ i ] != null)
54
        ? createMenuItem( actions[ i ] )
55
        : new SeparatorMenuItem();
56
    }
57
    return menuItems;
58
  }
59
60
  public static MenuItem createMenuItem( Action action ) {
61
    MenuItem menuItem = new MenuItem( action.text );
62
    if( action.accelerator != null ) {
63
      menuItem.setAccelerator( action.accelerator );
64
    }
65
66
    if( action.icon != null ) {
67
      menuItem.setGraphic( FontAwesomeIconFactory.get().createIcon( action.icon ) );
68
    }
69
70
    menuItem.setOnAction( action.action );
71
72
    if( action.disable != null ) {
73
      menuItem.disableProperty().bind( action.disable );
74
    }
75
76
    menuItem.setMnemonicParsing( true );
77
78
    return menuItem;
79
  }
80
81
  public static ToolBar createToolBar( Action... actions ) {
82
    return new ToolBar( createToolBarButtons( actions ) );
83
  }
84
85
  public static Node[] createToolBarButtons( Action... actions ) {
86
    Node[] buttons = new Node[ actions.length ];
87
    for( int i = 0; i < actions.length; i++ ) {
88
      buttons[ i ] = (actions[ i ] != null)
89
        ? createToolBarButton( actions[ i ] )
90
        : new Separator();
91
    }
92
    return buttons;
93
  }
94
95
  public static Button createToolBarButton( Action action ) {
96
    Button button = new Button();
97
    button.setGraphic( FontAwesomeIconFactory.get().createIcon( action.icon, "1.2em" ) );
98
    String tooltip = action.text;
99
    if( tooltip.endsWith( "..." ) ) {
100
      tooltip = tooltip.substring( 0, tooltip.length() - 3 );
101
    }
102
    if( action.accelerator != null ) {
103
      tooltip += " (" + action.accelerator.getDisplayText() + ')';
104
    }
105
    button.setTooltip( new Tooltip( tooltip ) );
106
    button.setFocusTraversable( false );
107
    button.setOnAction( action.action );
108
    if( action.disable != null ) {
109
      button.disableProperty().bind( action.disable );
110
    }
111
    return button;
112
  }
113
}
1114
A src/main/java/com/scrivenvar/util/Item.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
/**
30
 * Simple item for a ChoiceBox, ComboBox or ListView. Consists of a string name
31
 * and a value object. toString() returns the name. equals() compares the value
32
 * and hashCode() returns the hash code of the value.
33
 *
34
 * @author Karl Tauber
35
 * @param <V> The type of item value.
36
 */
37
public class Item<V> {
38
39
  public final String name;
40
  public final V value;
41
42
  public Item( final String name, final V value ) {
43
    this.name = name;
44
    this.value = value;
45
  }
46
47
  @Override
48
  public boolean equals( final Object obj ) {
49
    if( this == obj ) {
50
      return true;
51
    }
52
    if( !(obj instanceof Item) ) {
53
      return false;
54
    }
55
    return Utils.safeEquals( value, ((Item<?>)obj).value );
56
  }
57
58
  @Override
59
  public int hashCode() {
60
    return (value != null) ? value.hashCode() : 0;
61
  }
62
63
  @Override
64
  public String toString() {
65
    return name;
66
  }
67
}
168
A src/main/java/com/scrivenvar/util/Lists.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.util;
29
30
import java.util.List;
31
32
/**
33
 * Convenience class that provides a clearer API for obtaining list elements.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public final class Lists {
38
39
  private Lists() {
40
  }
41
42
  /**
43
   * Returns the first item in the given list, or null if not found.
44
   *
45
   * @param <T> The generic list type.
46
   * @param list The list that may have a first item.
47
   *
48
   * @return null if the list is null or there is no first item.
49
   */
50
  public static <T> T getFirst( final List<T> list ) {
51
    return getFirst( list, null );
52
  }
53
54
  /**
55
   * Returns the last item in the given list, or null if not found.
56
   *
57
   * @param <T> The generic list type.
58
   * @param list The list that may have a last item.
59
   *
60
   * @return null if the list is null or there is no last item.
61
   */
62
  public static <T> T getLast( final List<T> list ) {
63
    return getLast( list, null );
64
  }
65
66
  /**
67
   * Returns the first item in the given list, or t if not found.
68
   *
69
   * @param <T> The generic list type.
70
   * @param list The list that may have a first item.
71
   * @param t The default return value.
72
   *
73
   * @return null if the list is null or there is no first item.
74
   */
75
  public static <T> T getFirst( final List<T> list, final T t ) {
76
    return isEmpty( list ) ? t : list.get( 0 );
77
  }
78
79
  /**
80
   * Returns the last item in the given list, or t if not found.
81
   *
82
   * @param <T> The generic list type.
83
   * @param list The list that may have a last item.
84
   * @param t The default return value.
85
   *
86
   * @return null if the list is null or there is no last item.
87
   */
88
  public static <T> T getLast( final List<T> list, final T t ) {
89
    return isEmpty( list ) ? t : list.get( list.size() - 1 );
90
  }
91
92
  /**
93
   * Returns true if the given list is null or empty.
94
   *
95
   * @param <T> The generic list type.
96
   * @param list The list that has a last item.
97
   *
98
   * @return true The list is empty.
99
   */
100
  public static <T> boolean isEmpty( final List<T> list ) {
101
    return list == null || list.isEmpty();
102
  }
103
}
1104
A src/main/java/com/scrivenvar/util/StageState.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import java.util.prefs.Preferences;
30
import javafx.application.Platform;
31
import javafx.scene.shape.Rectangle;
32
import javafx.stage.Stage;
33
import javafx.stage.WindowEvent;
34
35
/**
36
 * Saves and restores Stage state (window bounds, maximized, fullScreen).
37
 *
38
 * @author Karl Tauber
39
 */
40
public class StageState {
41
42
  public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition";
43
  public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor";
44
  public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview";
45
46
  private final Stage stage;
47
  private final Preferences state;
48
49
  private Rectangle normalBounds;
50
  private boolean runLaterPending;
51
52
  public StageState( final Stage stage, final Preferences state ) {
53
    this.stage = stage;
54
    this.state = state;
55
56
    restore();
57
58
    stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() );
59
60
    stage.xProperty().addListener( (ob, o, n) -> boundsChanged() );
61
    stage.yProperty().addListener( (ob, o, n) -> boundsChanged() );
62
    stage.widthProperty().addListener( (ob, o, n) -> boundsChanged() );
63
    stage.heightProperty().addListener( (ob, o, n) -> boundsChanged() );
64
  }
65
66
  private void save() {
67
    final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds;
68
    
69
    if( bounds != null ) {
70
      state.putDouble( "windowX", bounds.getX() );
71
      state.putDouble( "windowY", bounds.getY() );
72
      state.putDouble( "windowWidth", bounds.getWidth() );
73
      state.putDouble( "windowHeight", bounds.getHeight() );
74
    }
75
    
76
    state.putBoolean( "windowMaximized", stage.isMaximized() );
77
    state.putBoolean( "windowFullScreen", stage.isFullScreen() );
78
  }
79
80
  private void restore() {
81
    final double x = state.getDouble( "windowX", Double.NaN );
82
    final double y = state.getDouble( "windowY", Double.NaN );
83
    final double w = state.getDouble( "windowWidth", Double.NaN );
84
    final double h = state.getDouble( "windowHeight", Double.NaN );
85
    final boolean maximized = state.getBoolean( "windowMaximized", false );
86
    final boolean fullScreen = state.getBoolean( "windowFullScreen", false );
87
88
    if( !Double.isNaN( x ) && !Double.isNaN( y ) ) {
89
      stage.setX( x );
90
      stage.setY( y );
91
    } // else: default behavior is center on screen
92
93
    if( !Double.isNaN( w ) && !Double.isNaN( h ) ) {
94
      stage.setWidth( w );
95
      stage.setHeight( h );
96
    } // else: default behavior is use scene size
97
98
    if( fullScreen != stage.isFullScreen() ) {
99
      stage.setFullScreen( fullScreen );
100
    }
101
    
102
    if( maximized != stage.isMaximized() ) {
103
      stage.setMaximized( maximized );
104
    }
105
  }
106
107
  /**
108
   * Remembers the window bounds when the window is not iconified, maximized or
109
   * in fullScreen.
110
   */
111
  private void boundsChanged() {
112
    // avoid too many (and useless) runLater() invocations
113
    if( runLaterPending ) {
114
      return;
115
    }
116
    
117
    runLaterPending = true;
118
119
    // must use runLater() to ensure that change of all properties
120
    // (x, y, width, height, iconified, maximized and fullScreen)
121
    // has finished
122
    Platform.runLater( () -> {
123
      runLaterPending = false;
124
125
      if( isNormalState() ) {
126
        normalBounds = getStageBounds();
127
      }
128
    } );
129
  }
130
131
  private boolean isNormalState() {
132
    return !stage.isIconified() && !stage.isMaximized() && !stage.isFullScreen();
133
  }
134
135
  private Rectangle getStageBounds() {
136
    return new Rectangle( stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight() );
137
  }
138
}
1139
A src/main/java/com/scrivenvar/util/Utils.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import java.util.ArrayList;
30
import java.util.prefs.Preferences;
31
32
/**
33
 * @author Karl Tauber
34
 */
35
public class Utils {
36
37
  public static boolean safeEquals( final Object o1, final Object o2 ) {
38
    if( o1 == o2 ) {
39
      return true;
40
    }
41
    if( o1 == null || o2 == null ) {
42
      return false;
43
    }
44
    return o1.equals( o2 );
45
  }
46
47
  public static boolean isNullOrEmpty( final String s ) {
48
    return s == null || s.isEmpty();
49
  }
50
51
  public static String ltrim( final String s ) {
52
    int i = 0;
53
54
    while( i < s.length() && Character.isWhitespace( s.charAt( i ) ) ) {
55
      i++;
56
    }
57
58
    return s.substring( i );
59
  }
60
61
  public static String rtrim( final String s ) {
62
    int i = s.length() - 1;
63
64
    while( i >= 0 && Character.isWhitespace( s.charAt( i ) ) ) {
65
      i--;
66
    }
67
68
    return s.substring( 0, i + 1 );
69
  }
70
71
  public static void putPrefs( Preferences prefs, String key, String value, String def ) {
72
    if( value != def && !value.equals( def ) ) {
73
      prefs.put( key, value );
74
    } else {
75
      prefs.remove( key );
76
    }
77
  }
78
79
  public static void putPrefsInt( Preferences prefs, String key, int value, int def ) {
80
    if( value != def ) {
81
      prefs.putInt( key, value );
82
    } else {
83
      prefs.remove( key );
84
    }
85
  }
86
87
  public static void putPrefsBoolean( Preferences prefs, String key, boolean value, boolean def ) {
88
    if( value != def ) {
89
      prefs.putBoolean( key, value );
90
    } else {
91
      prefs.remove( key );
92
    }
93
  }
94
95
  public static String[] getPrefsStrings( final Preferences prefs, String key ) {
96
    final ArrayList<String> arr = new ArrayList<>( 256 );
97
98
    for( int i = 0; i < 10000; i++ ) {
99
      final String s = prefs.get( key + (i + 1), null );
100
101
      if( s == null ) {
102
        break;
103
      }
104
105
      arr.add( s );
106
    }
107
108
    return arr.toArray( new String[ arr.size() ] );
109
  }
110
111
  public static void putPrefsStrings( Preferences prefs, String key, String[] strings ) {
112
    for( int i = 0; i < strings.length; i++ ) {
113
      prefs.put( key + (i + 1), strings[ i ] );
114
    }
115
116
    for( int i = strings.length; prefs.get( key + (i + 1), null ) != null; i++ ) {
117
      prefs.remove( key + (i + 1) );
118
    }
119
  }
120
}
1121
A src/main/r/README.md
1
# R Scripts
2
3
These R scripts illustrate how R can be used within an application to perform calculations using variables. Authors are free to write their own scripts, of course. These scripts serve as an example of how to automate certain tasks while writing.
4
5
## Configuration
6
7
Configure the editor to use the R scripts as follows:
8
9
1. Copy the R scripts into same directory as your Markdown files.
10
1. Start the editor.
11
1. Click **Tools → R Script**.
12
1. Copy and paste the following:
13
14
        assign( 'anchor', as.Date( '$date.anchor$', format='%Y-%m-%d' ), envir = .GlobalEnv );
15
        setwd( '$application.r.working.directory$' );
16
        source( 'pluralize.R' );
17
        source( 'csv.R' );
18
        source( 'conversion.R' );
19
20
1. Click **File → New** to create a new file.
21
1. Click **File → Save As** to set a filename.
22
1. Set **Name** to: `variables.yaml`
23
1. Click **OK**.
24
1. Paste the following definitions:
25
26
        date:
27
          anchor: 2017-01-01
28
        editor:
29
          examples:
30
            season: 2017-09-02
31
            math:
32
              x: 1
33
              y: $editor.examples.math.x$ + 1
34
              z: $editor.examples.math.y$ + 1
35
            name:
36
              given: Josephene
37
38
1. Save and close the file.
39
1. Click **File → Open**
40
1. Change **Markdown Files** to **Definition Files**.
41
1. Select `variables.yaml`.
42
1. Click **Open**.
43
44
R functionality is configured.
45
46
## Definitions
47
48
The variables definitions within `variables.yaml` are available to R using the R syntax. An additional variable, `application.r.working.directory` is added to the list of variables. The value is set to the working directory of the file being edited. Hover the mouse cursor over the file tab in the editor to see the full path to the file.
49
50
## Examples
51
52
This section demonstrates how to use the R functions when editing. Complete the following steps to begin:
53
54
1. Click **File → New** to create a new file.
55
1. Click **File → Save As** to set a filename.
56
1. Set **Name** to: `example.Rmd`
57
1. Click **OK**.
58
59
The examples are ready for use within the editor.
60
61
### Arithmetic
62
63
Type the following to perform a simple calculation:
64
65
    `r# 1+1`
66
67
The preview pane shows `2.0`.
68
69
### Functions
70
71
Call the [format](https://stat.ethz.ch/R-manual/R-devel/library/base/html/format.html) function to truncate unwanted decimal places as follows:
72
73
    `r# format(1+1,digits=1)`
74
75
The preview pane shows `2`.
76
77
### Pluralize
78
79
Many English words can be pluralized as follows:
80
81
    `r# pl('wolf',2)`
82
83
The preview pane shows `wolves`. The `pluralize.R` file contains a partial implementation of Damian Conway's algorithmic approach to English pluralization.
84
85
### Chicago Manual of Style
86
87
Apply the Chicago Manual of Style for words less than one-hundred as follows:
88
89
       `r# cms(1)` `r# cms(99)` `r# cms(101)`
90
91
The preview pane shows numbers written out as `one` and `ninety-nine`, followed by the digits 101.
92
93
### Data Import
94
95
Import and display information from a CSV file as follows:
96
97
1. Click **File → New** to create a new file.
98
1. Click **File → Save As** to rename the file.
99
1. Set the filename to: `data.csv`
100
1. Paste the following into `data.csv`:
101
102
        Animal,Quantity,Country
103
        Aardwolf,1,Africa
104
        Keel-billed toucan,1,Belize
105
        Beaver,2,Canada
106
        Mute swan,3,Denmark
107
        Lion,5,Ethiopia
108
        Brown bear,8,Finland
109
        Dolphin,13,Greece
110
        Turul,21,Hungary
111
        Gyrfalcon,34,Iceland
112
        Red-billed streamertail,55,Jamaica
113
114
1. Click the `example.Rmd` tab.
115
1. Type the following:
116
117
       `r# csv2md('data.csv',total=F)`
118
119
1. Type the following to calculate a total for all numeric columns:
120
121
       `r# csv2md('data.csv')`
122
123
This imports the data from an external file and formats the information into a table, automatically. Update the data as follows:
124
125
1. Click the `data.csv` tab to edit the data.
126
1. Change the data by adding a new row.
127
1. Save the file.
128
1. Click the `example.Rmd` tab.
129
130
The preview pane shows the revised contents.
131
132
### Elapsed Time
133
134
The duration of a timeline, given in numbers of days, can be computed into English as follows:
135
136
    `r# elapsed(1,1)`
137
138
The preview pane shows `same day`. Change the expression to:
139
140
    `r# elapsed(1,2)`
141
142
The preview pane shows `one day`. Change the expression to:
143
144
    `r# elapsed(1,112358)`
145
146
The preview pane shows `307 years, seven months, and sixteen days`, combined using the Chicago Manual of Style, the pluralization function, and a [serial comma](https://www.behance.net/gallery/19417363/The-Oxford-Comma).
147
148
### Variable Syntax
149
150
The syntax for a variable changes when using an R Markdown file (denoted by the `.Rmd` filename extension), as opposed to a regular Markdown file (`.md`). Return to the example file and type the following:
151
152
    `r# v$date$anchor`
153
154
The preview pane shows the date.
155
156
### Autocomplete
157
158
Automatically insert a variable reference into the text as follows:
159
160
1. Type: `Jos`
161
    * Note the capital letter, matches are case sensitive.
162
1. Hold down the `Control` key.
163
1. Tap the `Spacebar`
164
165
The editor shows:
166
167
    `r#x( v$editor$examples$name$given )`
168
169
The preview pane shows:
170
171
    Josephine
172
173
Here, the `x` function evaluates its parameter as an expression. This allows variables to include expressions in their definition.
174
175
### Variable Definition Expressions
176
177
Definition file variables are have the ability to reference other definitions. Try the following:
178
179
    x = `r#x( v$editor$examples$math$x )`;
180
    y = `r#x( v$editor$examples$math$y )`;
181
    z = `r#x( v$editor$examples$math$z )`
182
183
The preview pane shows:
184
185
    x = 1.0; y = 2.0; z = 3.0
186
187
### Case
188
189
Ensure words begin with a lowercase letter as follows:
190
191
    `r#lc( v$editor$examples$name$given )`
192
193
The preview pane shows:
194
195
    josephine
196
197
Similarly, ensure an uppercase letter as follows:
198
199
    `r#uc( 'hello, world!' )`
200
201
The preview pane shows:
202
203
    Hello, world!
204
205
### Month
206
207
Display the month name given a month number as follows:
208
209
    `r# month( 1 )`
210
211
The preview pane shows:
212
213
    January
214
215
## Summary
216
217
Authors can inline R statements into documents, directly, so long as those statements generate text. Plots, graphs, and images must be referenced as external image files or URLs.
1218
A src/main/r/conversion.R
1
# ########################################################################
2
#
3
# Substitute R expressions in a document with their evaluated value. The
4
# anchor variable must be set for functions that use relative dates.
5
#
6
# ########################################################################
7
8
# Evaluates an expression; writes s if there is no expression.
9
x <- function( s ) {
10
  return(
11
    tryCatch({
12
      r = eval( parse( text=s ) )
13
14
      # If the result isn't primitive, then it was probably parsed into
15
      # an unprintable object (e.g., "gray" becomes a colour). In those
16
      # cases, return the original text string. Otherwise, an atomic
17
      # value means a primitive type (string, integer, etc.) that can be
18
      # written directly into the document.
19
      #
20
      # See: http://stackoverflow.com/a/19501276/59087
21
      if( is.atomic( r ) ) {
22
        r
23
      }
24
      else {
25
        s
26
      }
27
    },
28
    warning = function( w ) {
29
      s
30
    },
31
    error = function( e ) {
32
      s
33
    })
34
  )
35
}
36
37
# Returns a date offset by a given number of days, relative to the given
38
# date (d). This does not use the anchor, but is used to get the anchor's
39
# value as a date.
40
when <- function( d, n = 0, format = "%Y-%m-%d" ) {
41
  as.Date( d, format = format ) + x( n )
42
}
43
44
# Full date (s) offset by an optional number of days before or after.
45
# This will remove leading zeros (applying leading spaces instead, which
46
# are ignored by any worthwhile typesetting engine).
47
annal <- function( days = 0, format = "%Y-%m-%d", oformat = "%B %d, %Y" ) {
48
  format( when( anchor, days ), format = oformat )
49
}
50
51
# Extracts the year from a date string.
52
year <- function( days = 0, format = "%Y-%m-%d" ) {
53
  annal( days, format, "%Y" )
54
}
55
56
# Day of the week (in days since the anchor date).
57
weekday <- function( n ) {
58
  weekdays( when( anchor, n ) )
59
}
60
61
# String concatenate function alias because paste0 is a terrible name.
62
concat <- paste0
63
64
# Translates a number from digits to words using Chicago Manual of Style.
65
# This does not translate numbers greater than one hundred. If ordinal
66
# is TRUE, this will return the ordinal name. This will not produce ordinals
67
# for numbers greater than 100
68
cms <- function( n, ordinal = FALSE ) {
69
  n <- x( n )
70
71
  # We're done here.
72
  if( n == 0 ) {
73
    if( ordinal ) {
74
      return( "zeroth" )
75
    }
76
77
    return( "zero" )
78
  }
79
80
  # Concatenate this a little later.
81
  if( n < 0 ) {
82
    result = "negative "
83
    n = abs( n )
84
  }
85
86
  # Do not spell out numbers greater than one hundred.
87
  if( n > 100 ) {
88
    # Comma-separated numbers.
89
    return( format( n, big.mark=",", trim=TRUE, scientific=FALSE ) )
90
  }
91
92
  # Don't go beyond 100.
93
  if( n == 100 ) {
94
    if( ordinal ) {
95
      return( "one hundredth" )
96
    }
97
98
    return( "one hundred" )
99
  }
100
101
  # Samuel Langhorne Clemens noted English has too many exceptions.
102
  small = c(
103
    "one", "two", "three", "four", "five",
104
    "six", "seven", "eight", "nine", "ten",
105
    "eleven", "twelve", "thirteen", "fourteen", "fifteen",
106
    "sixteen", "seventeen", "eighteen", "nineteen"
107
  )
108
109
  ord_small = c(
110
    "first", "second", "third", "fourth", "fifth",
111
    "sixth", "seventh", "eighth", "ninth", "tenth",
112
    "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth",
113
    "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth"
114
  )
115
116
  # After this, the number (n) is between 20 and 99.
117
  if( n < 20 ) {
118
    if( ordinal ) {
119
      return( .subset( ord_small, n %% 100 ) )
120
    }
121
122
    return( .subset( small, n %% 100 ) )
123
  }
124
125
  tens = c( "",
126
    "twenty", "thirty", "forty", "fifty",
127
    "sixty", "seventy", "eighty", "ninety"
128
  )
129
130
  ord_tens = c( "",
131
    "twentieth", "thirtieth", "fortieth", "fiftieth",
132
    "sixtieth", "seventieth", "eightieth", "ninetieth"
133
  )
134
135
  ones_index = n %% 10
136
  n = n %/% 10
137
138
  # No number in the ones column, so the number must be a multiple of ten.
139
  if( ones_index == 0 ) {
140
    if( ordinal ) {
141
      return( .subset( ord_tens, n ) )
142
    }
143
144
    return( .subset( tens, n ) )
145
  }
146
147
  # Find the value from the ones column.
148
  if( ordinal ) {
149
    unit_1 = .subset( ord_small, ones_index )
150
  }
151
  else {
152
    unit_1 = .subset( small, ones_index )
153
  }
154
155
  # Find the tens column.
156
  unit_10 = .subset( tens, n )
157
158
  # Hyphenate the tens and the ones together.
159
  concat( unit_10, concat( "-", unit_1 ) )
160
}
161
162
# Returns a human-readable string that provides the elapsed time between
163
# two numbers in terms of years, months, and days. If any unit value is zero,
164
# the unit is not included. The words (year, month, day) are pluralized
165
# according to English grammar. The numbers are written out according to
166
# Chicago Manual of Style. This applies the serial comma.
167
#
168
# Both numbers are offsets relative to the anchor date.
169
#
170
# If all unit values are zero, this returns s ("same day" by default).
171
#
172
# If the start date (began) is greater than end date (ended), the dates are
173
# swapped before calculations are performed. This allows any two dates
174
# to be compared and positive unit values are always returned.
175
#
176
elapsed <- function( began, ended, s = "same day" ) {
177
  began = when( anchor, began )
178
  ended = when( anchor, ended )
179
180
  # Swap the dates if the end date comes before the start date.
181
  if( as.integer( ended - began ) < 0 ) {
182
    tempd = began
183
    began = ended
184
    ended = tempd
185
  }
186
187
  # Calculate number of elapsed years.
188
  years = length( seq( from = began, to = ended, by = 'year' ) ) - 1
189
190
  # Move the start date up by the number of elapsed years.
191
  if( years > 0 ) {
192
    began = seq( began, length = 2, by = concat( years, " years" ) )[2]
193
    years = pl.numeric( "year", years )
194
  }
195
  else {
196
    # Zero years.
197
    years = ""
198
  }
199
200
  # Calculate number of elapsed months, excluding years.
201
  months = length( seq( from = began, to = ended, by = 'month' ) ) - 1
202
203
  # Move the start date up by the number of elapsed months
204
  if( months > 0 ) {
205
    began = seq( began, length = 2, by = concat( months, " months" ) )[2]
206
    months = pl.numeric( "month", months )
207
  }
208
  else {
209
    # Zero months
210
    months = ""
211
  }
212
213
  # Calculate number of elapsed days, excluding months and years.
214
  days = length( seq( from = began, to = ended, by = 'day' ) ) - 1
215
216
  if( days > 0 ) {
217
    days = pl.numeric( "day", days )
218
  }
219
  else {
220
    # Zero days
221
    days = ""
222
  }
223
224
  if( years <= 0 && months <= 0 && days <= 0 ) {
225
    return( s )
226
  }
227
228
  # Put them all in a vector, then remove the empty values.
229
  s <- c( years, months, days )
230
  s <- s[ s != "" ]
231
232
  r <- paste( s, collapse = ", " )
233
234
  # If all three items are present, replace the last comma with ", and".
235
  if( length( s ) > 2 ) {
236
    return( gsub( "(.*),", "\\1, and", r ) )
237
  }
238
239
  # Does nothing if no commas are present.
240
  gsub( "(.*),", "\\1 and", r )
241
}
242
243
# Returns the number (n) in English followed by the plural or singular
244
# form of the given string (s; resumably a noun), if applicable, according
245
# to English grammar. That is, pl.numeric( "wolf", 5 ) will return
246
# "five wolves".
247
pl.numeric <- function( s, n ) {
248
  concat( cms( n ), concat( " ", pluralize( s, n ) ) )
249
}
250
251
# Name of the season, starting with an capital letter.
252
season <- function( n, format = "%Y-%m-%d" ) {
253
  WS <- as.Date("2016-12-15", "%Y-%m-%d") # Winter Solstice
254
  SE <- as.Date("2016-03-15", "%Y-%m-%d") # Spring Equinox
255
  SS <- as.Date("2016-06-15", "%Y-%m-%d") # Summer Solstice
256
  AE <- as.Date("2016-09-15", "%Y-%m-%d") # Autumn Equinox
257
258
  d <- when( anchor, n )
259
  d <- as.Date( strftime( d, format="2016-%m-%d" ) )
260
261
  ifelse( d >= WS | d < SE, "Winter",
262
    ifelse( d >= SE & d < SS, "Spring",
263
      ifelse( d >= SS & d < AE, "Summer", "Autumn" )
264
    )
265
  )
266
}
267
268
# Converts the first letter in a string to lowercase
269
lc <- function( s ) {
270
  concat( tolower( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
271
}
272
273
# Converts the first letter in a string to uppercase
274
uc <- function( s ) {
275
  concat( toupper( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
276
}
277
278
# Returns the number of days between the given dates.
279
days <- function( d1, d2, format = "%Y-%m-%d" ) {
280
  dates = c( d1, d2 )
281
  dt = strptime( dates, format = format )
282
  as.integer( difftime( dates[2], dates[1], units = "days" ) )
283
}
284
285
# Returns the number of years elapsed.
286
years <- function( began, ended ) {
287
  began = when( anchor, began )
288
  ended = when( anchor, ended )
289
290
  # Swap the dates if the end date comes before the start date.
291
  if( as.integer( ended - began ) < 0 ) {
292
    tempd = began
293
    began = ended
294
    ended = tempd
295
  }
296
297
  # Calculate number of elapsed years.
298
  length( seq( from = began, to = ended, by = 'year' ) ) - 1
299
}
300
301
# Full name of the month, starting with a capital letter.
302
month <- function( n ) {
303
  # Faster than month.name[ x( n ) ]
304
  .subset( month.name, x( n ) )
305
}
306
307
money <- function( n ) {
308
  formatC( x( n ), format="d" )
309
}
1310
A src/main/r/csv.R
1
# ######################################################################
2
#
3
# Copyright 2016, White Magic Software, Ltd.
4
# 
5
# Permission is hereby granted, free of charge, to any person obtaining
6
# a copy of this software and associated documentation files (the
7
# "Software"), to deal in the Software without restriction, including
8
# without limitation the rights to use, copy, modify, merge, publish,
9
# distribute, sublicense, and/or sell copies of the Software, and to
10
# permit persons to whom the Software is furnished to do so, subject to
11
# the following conditions:
12
# 
13
# The above copyright notice and this permission notice shall be
14
# included in all copies or substantial portions of the Software.
15
# 
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
#
24
# ######################################################################
25
26
# ######################################################################
27
#
28
# Converts CSV to Markdown.
29
#
30
# ######################################################################
31
32
# Reads a CSV file and converts the contents to a Markdown table. The
33
# file must be in the working directory as specified by setwd.
34
#
35
# @param f The filename to convert.
36
# @param decimals Rounded decimal places (default 1).
37
# @param totals Include total sums (default TRUE).
38
# @param align Right-align numbers (default TRUE).
39
csv2md <- function( f, decimals = 1, totals = T, align = T ) {
40
  # Read the CVS data from the file; ensure strings become characters.
41
  df <- read.table( f, sep=',', header=T, stringsAsFactors=F )
42
43
  if( totals ) {
44
    # Determine what columns can be summed.
45
    number <- which( unlist( lapply( df, is.numeric ) ) )
46
47
    # Use colSums when more than one summable column exists.
48
    if( length( number ) > 1 ) {
49
      f.sum <- colSums
50
    }
51
    else {
52
      f.sum <- sum
53
    }
54
55
    # Calculate the sum of all the summable columns and insert the
56
    # results back into the data frame.
57
    df[ (nrow( df ) + 1), number ] <- f.sum( df[, number], na.rm=TRUE )
58
59
    # pluralize would be heavyweight here.
60
    if( length( number ) > 1 ) {
61
      t <- "**Totals**"
62
    }
63
    else {
64
      t <- "**Total**"
65
    }
66
67
    # Change the first column of the last line to "Total(s)".
68
    df[ nrow( df ), 1 ] <- t
69
70
    # Don't clutter the output with "NA" text.
71
    df[ is.na( df ) ] <- ""
72
  }
73
74
  if( align ) {
75
    is.char <- vapply( df, is.character, logical( 1 ) )
76
    dashes <- paste( ifelse( is.char, ':---', '---:' ), collapse='|' )
77
  }
78
  else {
79
    dashes <- paste( rep( '---', length( df ) ), collapse = '|')
80
  }
81
82
  # Create a Markdown version of the data frame.
83
  paste(
84
    paste( names( df ), collapse = '|'), '\n',
85
    dashes, '\n', 
86
    paste(
87
      Reduce( function( x, y ) {
88
          paste( x, format( y, digits = decimals ), sep = '|' )
89
        }, df
90
      ),
91
      collapse = '|\n', sep=''
92
    )
93
  )
94
}
95
196
A src/main/r/pluralize.R
1
# ######################################################################
2
#
3
# Copyright 2016, White Magic Software, Ltd.
4
# 
5
# Permission is hereby granted, free of charge, to any person obtaining
6
# a copy of this software and associated documentation files (the
7
# "Software"), to deal in the Software without restriction, including
8
# without limitation the rights to use, copy, modify, merge, publish,
9
# distribute, sublicense, and/or sell copies of the Software, and to
10
# permit persons to whom the Software is furnished to do so, subject to
11
# the following conditions:
12
# 
13
# The above copyright notice and this permission notice shall be
14
# included in all copies or substantial portions of the Software.
15
# 
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
#
24
# ######################################################################
25
26
# ######################################################################
27
#
28
# See Damian Conway's "An Algorithmic Approach to English Pluralization":
29
#   http://goo.gl/oRL4MP
30
# See Oliver Glerke's Evo Inflector: https://github.com/atteo/evo-inflector/
31
# See Shevek's Pluralizer: https://github.com/shevek/linguistics/
32
# See also: http://www.freevectors.net/assets/files/plural.txt
33
#
34
# ######################################################################
35
36
pluralize <- function( s, n ) {
37
  result <- s
38
39
  # Partial implementation of Conway's algorithm for nouns.
40
  if( n != 1 ) {
41
    if( pl.noninflective( s ) ||
42
        pl.suffix( "fish", s ) ||
43
        pl.suffix( "ois", s ) ||
44
        pl.suffix( "sheep", s ) ||
45
        pl.suffix( "deer", s ) ||
46
        pl.suffix( "pox", s ) ||
47
        pl.suffix( "[A-Z].*ese", s ) ||
48
        pl.suffix( "itis", s ) ) {
49
      # 1. Retain non-inflective user-mapped noun as is.
50
      # 2. Retain non-inflective plural as is.
51
      result <- s
52
    }
53
    else if( pl.is.irregular.pl( s ) ) {
54
      # 4. Change irregular plurals based on mapping.
55
      result <- pl.irregular.pl( s )
56
    }
57
    else if( pl.is.irregular.es( s ) ) {
58
      # x. From Shevek's Pluralizer
59
      result <- pl.inflect( s, "", "es" )
60
    }
61
    else if( pl.suffix( "man", s ) ) {
62
      # 5. For -man, change -an to -en
63
      result <- pl.inflect( s, "an", "en" )
64
    }
65
    else if( pl.suffix( "[lm]ouse", s ) ) {
66
      # 5. For [lm]ouse, change -ouse to -ice
67
      result <- pl.inflect( s, "ouse", "ice" )
68
    }
69
    else if( pl.suffix( "tooth", s ) ) {
70
      # 5. For -tooth, change -ooth to -eeth
71
      result <- pl.inflect( s, "ooth", "eeth" )
72
    }
73
    else if( pl.suffix( "goose", s ) ) {
74
      # 5. For -goose, change -oose to -eese
75
      result <- pl.inflect( s, "oose", "eese" )
76
    }
77
    else if( pl.suffix( "foot", s ) ) {
78
      # 5. For -foot, change -oot to -eet
79
      result <- pl.inflect( s, "oot", "eet" )
80
    }
81
    else if( pl.suffix( "zoon", s ) ) {
82
      # 5. For -zoon, change -on to -a
83
      result <- pl.inflect( s, "on", "a" )
84
    }
85
    else if( pl.suffix( "[csx]is", s ) ) {
86
      # 5. Change -cis, -sis, -xis to -es
87
      result <- pl.inflect( s, "is", "es" )
88
    }
89
    else if( pl.suffix( "([cs]h|ss)", s ) ) {
90
      # 8. Change -ch, -sh, -ss to -es
91
      result <- pl.inflect( s, "", "es" )
92
    }
93
    else if( pl.suffix( "([aeo]lf|[^d]eaf|arf)", s ) ) {
94
      # 9. Change -f to -ves
95
      result <- pl.inflect( s, "f", "ves" )
96
    }
97
    else if( pl.suffix( "[nlw]ife", s ) ) {
98
      # 9. Change -fe to -ves
99
      result <- pl.inflect( s, "fe", "ves" )
100
    }
101
    else if( pl.suffix( "([aeiou]y|[A-Z].*y)", s ) ) {
102
      # 10. Change -y to -ys.
103
      result <- pl.inflect( s, "", "s" )
104
    }
105
    else if( pl.suffix( "y", s ) ) {
106
      # 10. Change -y to -ies.
107
      result <- pl.inflect( s, "y", "ies" )
108
    }
109
    else {
110
      # 13. Default plural: add -s.
111
      result <- pl.inflect( s, "", "s" )
112
    }
113
  }
114
115
  result
116
}
117
118
# Pluralize s if n is not equal to 1.
119
pl <- function( s, n ) {
120
  pluralize( s, x( n ) )
121
}
122
123
# Returns the given string (s) with its suffix replaced by r.
124
pl.inflect <- function( s, suffix, r ) {
125
  gsub( paste( suffix, "$", sep="" ), r, s )
126
}
127
128
# Answers whether the given string (s) has the given ending.
129
pl.suffix <- function( ending, s ) {
130
  grepl( paste( ending, "$", sep="" ), s )
131
}
132
133
# Answers whether the given string (s) is a noninflective noun.
134
pl.noninflective <- function( s ) {
135
  v <- c(
136
    "aircraft", "Bhutanese", "bison", "bream", "breeches", "britches",
137
    "Burmese", "carp", "chassis", "Chinese", "clippers", "cod", "contretemps",
138
    "corps", "debris", "diabetes", "djinn", "eland", "elk", "flounder",
139
    "fracas", "gallows", "graffiti", "headquarters", "herpes", "high-jinks",
140
    "homework", "hovercraft", "innings", "jackanapes", "Japanese",
141
    "Lebanese", "mackerel", "means", "measles", "mews", "mumps", "news",
142
    "pincers", "pliers", "Portuguese", "proceedings", "rabies", "salmon",
143
    "scissors", "sea-bass", "Senegalese", "series", "shears", "Siamese",
144
    "Sinhalese", "spacecraft", "species", "swine", "trout", "tuna",
145
    "Vietnamese", "watercraft", "whiting", "wildebeest"
146
  )
147
148
  is.element( s, v )
149
}
150
151
# Answers whether the given string (s) is an irregular plural.
152
pl.is.irregular.pl <- function( s ) {
153
  # Could be refactored with pl.irregular.pl...
154
  v <- c(
155
    "beef", "brother", "child", "cow", "ephemeris", "genie", "money",
156
    "mongoose", "mythos", "octopus", "ox", "soliloquy", "trilby"
157
  )
158
159
  is.element( s, v )
160
}
161
162
# Call to pluralize an irregular noun. Only call after confirming
163
# the noun is irregular via pl.is.irregular.pl.
164
pl.irregular.pl <- function( s ) {
165
  v <- list(
166
    "beef" = "beefs",
167
    "brother" = "brothers",
168
    "child" = "children",
169
    "cow" = "cows",
170
    "ephemeris" = "ephemerides",
171
    "genie" = "genies",
172
    "money" = "moneys",
173
    "mongoose" = "mongooses",
174
    "mythos" = "mythoi",
175
    "octopus" = "octopuses",
176
    "ox" = "oxen",
177
    "soliloquy" = "soliloquies",
178
    "trilby" = "trilbys"
179
  )
180
181
  # Faster version of v[[ s ]]
182
  .subset2( v, s )
183
}
184
185
# Answers whether the given string (s) pluralizes with -es.
186
pl.is.irregular.es <- function( s ) {
187
  v <- c(
188
    "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis",
189
    "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais",
190
    "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris",
191
    "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis",
192
    "polis", "rhinoceros", "sassafrass", "trellis"
193
  )
194
195
  is.element( s, v )
196
}
197
1198
A src/main/resources/META-INF/services/com.scrivenvar.service.Options
1
1
com.scrivenvar.service.impl.DefaultOptions
A src/main/resources/META-INF/services/com.scrivenvar.service.Settings
1
1
com.scrivenvar.service.impl.DefaultSettings
A src/main/resources/META-INF/services/com.scrivenvar.service.Snitch
1
1
com.scrivenvar.service.impl.DefaultSnitch
A src/main/resources/META-INF/services/com.scrivenvar.service.events.Notifier
1
1
com.scrivenvar.service.events.impl.DefaultNotifier
A src/main/resources/com/scrivenvar/build.sh
1
#!/bin/bash
2
3
INKSCAPE="/usr/bin/inkscape"
4
PNG_COMPRESS="optipng"
5
PNG_COMPRESS_OPTS="-o9 *png"
6
ICO_TOOL="icotool"
7
ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png"
8
9
declare -a SIZES=("16" "32" "64" "128" "256" "512")
10
11
for i in "${SIZES[@]}"; do
12
  # -y: export background opacity 0
13
  $INKSCAPE -y 0 -z -f "logo.svg" -w "${i}" -e "logo${i}.png"
14
done
15
16
# Compess the PNG images.
17
which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS
18
19
# Generate an ICO file.
20
which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS
21
122
A src/main/resources/com/scrivenvar/editor/markdown.css
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
.markdown-editor {
29
  -fx-font-size: 14px;
30
}
31
32
/*---- headers ----*/
33
34
.markdown-editor .h1 { -fx-font-size: 2.25em; }
35
.markdown-editor .h2 { -fx-font-size: 1.75em; }
36
.markdown-editor .h3 { -fx-font-size: 1.5em; }
37
.markdown-editor .h4 { -fx-font-size: 1.25em; }
38
.markdown-editor .h5 { -fx-font-size: 1.1em; }
39
.markdown-editor .h6 { -fx-font-size: 1em; }
40
41
.markdown-editor .h1,
42
.markdown-editor .h2,
43
.markdown-editor .h3,
44
.markdown-editor .h4,
45
.markdown-editor .h5,
46
.markdown-editor .h6 {
47
  -fx-font-weight: bold;
48
  -fx-fill: derive(crimson, -20%);
49
}
50
51
52
/*---- inlines ----*/
53
54
.markdown-editor .strong {
55
  -fx-font-weight: bold;
56
}
57
58
.markdown-editor .em {
59
  -fx-font-style: italic;
60
}
61
62
.markdown-editor .del {
63
  -fx-strikethrough: true;
64
}
65
66
.markdown-editor .a {
67
  -fx-fill: #4183C4 !important;
68
}
69
70
.markdown-editor .img {
71
  -fx-fill: #4183C4 !important;
72
}
73
74
.markdown-editor .code {
75
  -fx-font-family: monospace;
76
  -fx-fill: #090 !important;
77
}
78
79
80
/*---- blocks ----*/
81
82
.markdown-editor .pre {
83
  -fx-font-family: monospace;
84
  -fx-fill: #060 !important;
85
}
86
87
.markdown-editor .blockquote {
88
  -fx-fill: #777;
89
}
90
91
92
/*---- lists ----*/
93
94
.markdown-editor .ul {
95
}
96
97
.markdown-editor .ol {
98
}
99
100
.markdown-editor .li {
101
  -fx-fill: #444;
102
}
103
104
.markdown-editor .dl {
105
}
106
107
.markdown-editor .dt {
108
  -fx-font-weight: bold;
109
  -fx-font-style: italic;
110
}
111
112
.markdown-editor .dd {
113
  -fx-fill: #444;
114
}
115
116
117
/*---- table ----*/
118
119
.markdown-editor .table {
120
  -fx-font-family: monospace;
121
}
122
123
.markdown-editor .thead {
124
}
125
126
.markdown-editor .tbody {
127
}
128
129
.markdown-editor .caption {
130
}
131
132
.markdown-editor .th {
133
  -fx-font-weight: bold;
134
}
135
136
.markdown-editor .tr {
137
}
138
139
.markdown-editor .td {
140
}
141
142
143
/*---- misc ----*/
144
145
.markdown-editor .html {
146
  -fx-font-family: monospace;
147
  -fx-fill: derive(crimson, -50%);
148
}
149
.markdown-editor .monospace {
150
  -fx-font-family: monospace;
151
}
1152
A src/main/resources/com/scrivenvar/logo.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4
<svg
5
   xmlns:dc="http://purl.org/dc/elements/1.1/"
6
   xmlns:cc="http://creativecommons.org/ns#"
7
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8
   xmlns:svg="http://www.w3.org/2000/svg"
9
   xmlns="http://www.w3.org/2000/svg"
10
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
   id="svg2"
13
   version="1.1"
14
   inkscape:version="0.91 r13725"
15
   width="512"
16
   height="512"
17
   viewBox="0 0 512 512"
18
   sodipodi:docname="logo.svg">
19
  <metadata
20
     id="metadata8">
21
    <rdf:RDF>
22
      <cc:Work
23
         rdf:about="">
24
        <dc:format>image/svg+xml</dc:format>
25
        <dc:type
26
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
27
        <dc:title></dc:title>
28
      </cc:Work>
29
    </rdf:RDF>
30
  </metadata>
31
  <defs
32
     id="defs6" />
33
  <sodipodi:namedview
34
     pagecolor="#ffffff"
35
     bordercolor="#666666"
36
     borderopacity="1"
37
     objecttolerance="10"
38
     gridtolerance="10"
39
     guidetolerance="10"
40
     inkscape:pageopacity="0"
41
     inkscape:pageshadow="2"
42
     inkscape:window-width="640"
43
     inkscape:window-height="480"
44
     id="namedview4"
45
     showgrid="false"
46
     fit-margin-top="0"
47
     fit-margin-left="0"
48
     fit-margin-right="0"
49
     fit-margin-bottom="0"
50
     inkscape:zoom="1.2682274"
51
     inkscape:cx="15.646213"
52
     inkscape:cy="213.34955"
53
     inkscape:current-layer="svg2" />
54
  <path
55
     style="fill:#ce6200;fill-opacity:1"
56
     d="m 203.2244,511.85078 c -60.01827,-1.2968 -121.688643,-6.5314 -192.436493,-16.334 -5.8078027,-0.8047 -10.66110747,-1.561 -10.78511762,-1.6806 -0.12404567,-0.1196 3.90488112,-4.5812 8.95313512,-9.9147 32.9484785,-34.8102 70.4314485,-73.8923 104.1521555,-108.5956 l 11.87611,-12.2221 5.48905,-10.2177 c 35.82801,-66.6927 75.13064,-128.5665 105.90637,-166.7277 6.13805,-7.611 10.21451,-12.0689 17.28719,-18.9048 36.6818,-35.4537 108.27279,-83.724003 206.0323,-138.917303 22.10365,-12.47935 51.93386,-28.64995037 52.26391,-28.33165037 0.38883,0.37499 -2.35932,25.95575037 -4.86585,45.29275037 -7.28943,56.236403 -17.04619,103.128903 -28.07642,134.939803 -7.19617,20.7536 -14.81287,35.152 -22.9667,43.4155 -3.60444,3.6529 -6.58328,5.7941 -10.1313,7.2825 l -2.56414,1.0756 -53.43164,0.1713 -53.43166,0.1713 3.69973,1.8547 c 26.78565,13.4282 52.58051,27.5241 59.57122,32.5533 4.48397,3.2259 4.41278,2.9854 1.59124,5.3784 -26.99514,22.8955 -74.52961,44.0013 -140.23089,62.2641 -26.34995,7.3244 -57.85469,14.6842 -86.99871,20.3237 l -10.26943,1.9871 -52.01052,53.2733 -52.010524,53.2732 -29.459801,15.1165 c -26.4100885,13.5517 -29.3446639,15.1388 -28.347645,15.3311 0.6117029,0.118 4.0894221,0.2188 7.7282726,0.2239 3.6388854,0.01 16.1273694,0.2329 27.7522124,0.5059 51.576376,1.2116 146.083985,1.512 170.154295,0.5409 34.66996,-1.3988 52.7606,-2.9325 67.58258,-5.7293 2.68664,-0.507 4.82907,-0.9755 4.76094,-1.0412 -0.0681,-0.066 -3.24733,-0.8833 -7.0649,-1.8169 -8.04133,-1.9664 -25.10167,-5.3107 -41.1231,-8.0612 -47.6405,-8.1787 -65.48708,-12.0107 -74.13028,-15.9169 -3.90548,-1.7651 -7.13816,-4.7659 -8.12937,-7.5463 -1.01822,-2.8562 -0.92214,-6.5271 0.23315,-8.9083 1.86563,-3.8451 6.14837,-6.7199 12.26745,-8.2345 16.96993,-4.2004 57.27977,-6.1832 90.36228,-4.4448 54.7332,2.8761 117.0767,13.1228 178.50212,29.3385 18.03514,4.7611 51.66065,14.656 51.22677,15.0744 -0.0824,0.08 -5.72762,-0.854 -12.54488,-2.0745 -40.1043,-7.18 -60.50854,-10.2888 -101.40822,-15.4507 -24.4851,-3.0902 -55.12614,-5.9915 -77.58876,-7.3465 -26.58826,-1.6039 -61.15821,-1.7754 -80.99202,-0.4019 l -3.19705,0.2214 8.70308,1.4934 c 51.89698,8.9047 77.51746,14.9877 88.00479,20.8948 6.9134,3.894 10.30497,9.4381 9.33333,15.2569 -1.50397,9.0066 -10.51381,14.0257 -32.00273,17.8278 -16.31374,2.8863 -47.27575,4.3845 -77.23553,3.7371 z"
57
     id="path4138" />
58
  <path
59
     style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"
60
     d="m 214.76931,324.51908 c 60.83777,-14.1145 111.89562,-31.6251 144.40025,-49.5229 3.12602,-1.7213 5.81747,-3.2537 5.98106,-3.4054 0.40534,-0.3759 -13.76388,-7.9415 -34.63489,-18.4929 -7.52161,-3.8026 -9.82337,-5.3787 -12.0735,-8.2668 -5.14485,-6.6036 -5.96081,-14.8404 -2.20331,-22.2417 1.80288,-3.5512 5.69484,-7.3007 9.36158,-9.019 5.20851,-2.4407 1.18148,-2.2865 59.71223,-2.2865 l 52.81361,0 2.13233,-2.1984 c 2.78673,-2.8731 5.23414,-6.4981 8.23035,-12.1905 14.14966,-26.8827 26.71842,-78.3816 36.24347,-148.503303 0.76704,-5.6468 1.36194,-10.2983 1.32201,-10.3369 -0.0399,-0.038 -5.47754,2.9629 -12.08361,6.6697 l -12.01104,6.7396 -133.83068,137.037303 c -73.60688,75.3705 -134.81732,138.0567 -136.0232,139.3026 l -2.19251,2.2653 8.254,-1.8067 c 4.53969,-0.9937 12.01053,-2.6783 16.60185,-3.7435 z"
61
     id="path4136" />
62
  <path
63
     style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"
64
     d="m 202.72524,284.43588 c 69.93294,-70.1332 135.4799,-131.9279 213.46406,-201.244203 7.71421,-6.8568 14.50542,-12.9341 15.09155,-13.5052 0.9482,-0.9239 0.96778,-0.9811 0.17761,-0.5188 -77.96496,45.611803 -139.23519,88.710503 -166.72539,117.278203 -18.81811,19.5556 -50.35654,64.861 -80.96704,116.3104 -0.91787,1.5427 1.02249,-0.3323 18.95921,-18.3204 z"
65
     id="path4142" />
66
  <path
67
     style="fill:#000000"
68
     d=""
69
     id="path4140"
70
     inkscape:connector-curvature="0" />
71
</svg>
172
A src/main/resources/com/scrivenvar/logo128.png
Binary file
A src/main/resources/com/scrivenvar/logo16.png
Binary file
A src/main/resources/com/scrivenvar/logo256.png
Binary file
A src/main/resources/com/scrivenvar/logo32.png
Binary file
A src/main/resources/com/scrivenvar/logo512.png
Binary file
A src/main/resources/com/scrivenvar/logo64.png
Binary file
A src/main/resources/com/scrivenvar/messages.properties
1
#
2
# Copyright 2017 Karl Tauber and White Magic Software, Ltd.
3
#
4
# All rights reserved.
5
#
6
# Redistribution and use in source and binary forms, with or without
7
# modification, are permitted provided that the following conditions are met:
8
#
9
#  * Redistributions of source code must retain the above copyright
10
#    notice, this list of conditions and the following disclaimer.
11
#
12
#  * Redistributions in binary form must reproduce the above copyright
13
#    notice, this list of conditions and the following disclaimer in the
14
#    documentation and/or other materials provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
#
28
29
# ########################################################################
30
#
31
# Main Application Window
32
#
33
# ########################################################################
34
35
# The application title should exist only once in the entire code base.
36
# All other references should either refer to this value via the Messages
37
# class, or indirectly using ${Main.title}.
38
Main.title=Scrivenvar
39
40
Main.menu.file=_File
41
Main.menu.file.new=_New
42
Main.menu.file.open=_Open...
43
Main.menu.file.close=_Close
44
Main.menu.file.close_all=Close All
45
Main.menu.file.save=_Save
46
Main.menu.file.save_as=Save _As
47
Main.menu.file.save_all=Save A_ll
48
Main.menu.file.exit=E_xit
49
50
Main.menu.edit=_Edit
51
Main.menu.edit.undo=_Undo
52
Main.menu.edit.redo=_Redo
53
Main.menu.edit.find=_Find
54
Main.menu.edit.find.replace=Re_place
55
Main.menu.edit.find.next=Find _Next
56
Main.menu.edit.find.previous=Find _Previous
57
58
Main.menu.insert=_Insert
59
Main.menu.insert.bold=Bold
60
Main.menu.insert.italic=Italic
61
Main.menu.insert.superscript=Superscript
62
Main.menu.insert.subscript=Subscript
63
Main.menu.insert.strikethrough=Strikethrough
64
Main.menu.insert.blockquote=Blockquote
65
Main.menu.insert.code=Inline Code
66
Main.menu.insert.fenced_code_block=Fenced Code Block
67
Main.menu.insert.fenced_code_block.prompt=Enter code here
68
Main.menu.insert.link=Link...
69
Main.menu.insert.image=Image...
70
Main.menu.insert.header_1=Header 1
71
Main.menu.insert.header_1.prompt=header 1
72
Main.menu.insert.header_2=Header 2
73
Main.menu.insert.header_2.prompt=header 2
74
Main.menu.insert.header_3=Header 3
75
Main.menu.insert.header_3.prompt=header 3
76
Main.menu.insert.header_4=Header 4
77
Main.menu.insert.header_4.prompt=header 4
78
Main.menu.insert.header_5=Header 5
79
Main.menu.insert.header_5.prompt=header 5
80
Main.menu.insert.header_6=Header 6
81
Main.menu.insert.header_6.prompt=header 6
82
Main.menu.insert.unordered_list=Unordered List
83
Main.menu.insert.ordered_list=Ordered List
84
Main.menu.insert.horizontal_rule=Horizontal Rule
85
86
Main.menu.tools=_Tools
87
Main.menu.tools.script=_R Script
88
89
Main.menu.help=_Help
90
Main.menu.help.about=About ${Main.title}
91
92
# ########################################################################
93
#
94
# Status Bar
95
#
96
# ########################################################################
97
98
Main.statusbar.text.offset=offset
99
Main.statusbar.line=Line {0} of {1}, ${Main.statusbar.text.offset} {2}
100
Main.statusbar.state.default=OK
101
Main.statusbar.parse.error={0} (near ${Main.statusbar.text.offset} {1})
102
103
# ########################################################################
104
#
105
# Definition Pane and its Tree View
106
#
107
# ########################################################################
108
109
Definition.menu.add=Add
110
Definition.menu.remove=Delete
111
112
# ########################################################################
113
#
114
# File Editor
115
#
116
# ########################################################################
117
118
FileEditor.untitled=Untitled
119
FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1}
120
FileEditor.loadFailed.title=Load
121
FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
122
FileEditor.saveFailed.title=Save
123
124
# ########################################################################
125
#
126
# File Open
127
#
128
# ########################################################################
129
130
Dialog.file.choose.open.title=Open File
131
Dialog.file.choose.save.title=Save File
132
133
Dialog.file.choose.filter.title.markdown=Markdown Files
134
Dialog.file.choose.filter.title.definition=Definition Files
135
Dialog.file.choose.filter.title.xml=XML Files
136
Dialog.file.choose.filter.title.all=All Files
137
138
# ########################################################################
139
#
140
# Alert Dialog
141
#
142
# ########################################################################
143
144
Alert.file.close.title=Close
145
Alert.file.close.text=Save changes to {0}?
146
147
# ########################################################################
148
#
149
# Definition Pane
150
#
151
# ########################################################################
152
153
Pane.defintion.node.root.title=Definitions
154
155
# Controls ###############################################################
156
157
# ########################################################################
158
#
159
# Browse Directory
160
#
161
# ########################################################################
162
163
BrowseDirectoryButton.chooser.title=Browse for local folder
164
BrowseDirectoryButton.tooltip=${BrowseDirectoryButton.chooser.title}
165
166
# ########################################################################
167
#
168
# Browse File
169
#
170
# ########################################################################
171
172
BrowseFileButton.chooser.title=Browse for local file
173
BrowseFileButton.chooser.allFilesFilter=All Files
174
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
175
176
# Dialogs ################################################################
177
178
# ########################################################################
179
#
180
# Image
181
#
182
# ########################################################################
183
184
Dialog.image.title=Image
185
Dialog.image.chooser.imagesFilter=Images
186
Dialog.image.previewLabel.text=Markdown Preview\:
187
Dialog.image.textLabel.text=Alternate Text\:
188
Dialog.image.titleLabel.text=Title (tooltip)\:
189
Dialog.image.urlLabel.text=Image URL\:
190
191
# ########################################################################
192
#
193
# Hyperlink
194
#
195
# ########################################################################
196
197
Dialog.link.title=Link
198
Dialog.link.previewLabel.text=Markdown Preview\:
199
Dialog.link.textLabel.text=Link Text\:
200
Dialog.link.titleLabel.text=Title (tooltip)\:
201
Dialog.link.urlLabel.text=Link URL\:
202
203
# ########################################################################
204
#
205
# About
206
#
207
# ########################################################################
208
209
Dialog.about.title=About
210
Dialog.about.header=${Main.title}
211
Dialog.about.content=Copyright 2017 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
212
213
# ########################################################################
214
#
215
# R Script
216
#
217
# ########################################################################
218
219
Dialog.rScript.title=R Startup Script
220
Dialog.rScript.content=Provide R statements to run prior to interpreting R statements embedded in the document.
221
222
# Options ################################################################
223
224
# ########################################################################
225
#
226
# Options Dialog
227
#
228
# ########################################################################
229
230
OptionsDialog.title=Options
231
OptionsDialog.generalTab.text=General
232
OptionsDialog.markdownTab.text=Markdown
233
234
# ########################################################################
235
#
236
# General Options Pane
237
#
238
# ########################################################################
239
240
GeneralOptionsPane.encodingLabel.text=En_coding\:
241
GeneralOptionsPane.lineSeparatorLabel.text=_Line separator\:
242
GeneralOptionsPane.lineSeparatorLabel2.text=(applies to new files only)
243
244
GeneralOptionsPane.platformDefault=Platform Default ({0})
245
GeneralOptionsPane.sepWindows=Windows (CRLF)
246
GeneralOptionsPane.sepUnix=Unix (LF)
247
248
# ########################################################################
249
#
250
# Markdown Options Pane
251
#
252
# ########################################################################
253
254
MarkdownOptionsPane.abbreviationsExtCheckBox.text=A_bbreviations in the way of
255
MarkdownOptionsPane.abbreviationsExtLink.text=Markdown Extra
256
MarkdownOptionsPane.anchorlinksExtCheckBox.text=_Anchor links in headers
257
MarkdownOptionsPane.atxHeaderSpaceExtCheckBox.text=Requires a space char after Atx \# header prefixes, so that \#dasdsdaf is not a header
258
MarkdownOptionsPane.autolinksExtCheckBox.text=_Plain (undelimited) autolinks in the way of
259
MarkdownOptionsPane.autolinksExtLink.text=Github-flavoured-Markdown
260
MarkdownOptionsPane.definitionListsExtCheckBox.text=_Definition lists in the way of
261
MarkdownOptionsPane.definitionListsExtLink.text=Markdown Extra
262
MarkdownOptionsPane.extAnchorLinksExtCheckBox.text=Generate anchor links for headers using complete contents of the header
263
MarkdownOptionsPane.fencedCodeBlocksExtCheckBox.text=_Fenced Code Blocks in the way of
264
MarkdownOptionsPane.fencedCodeBlocksExtLabel.text=or
265
MarkdownOptionsPane.fencedCodeBlocksExtLink.text=Markdown Extra
266
MarkdownOptionsPane.fencedCodeBlocksExtLink2.text=Github-flavoured-Markdown
267
MarkdownOptionsPane.forceListItemParaExtCheckBox.text=Force List and Definition Paragraph wrapping if it includes more than just a single paragraph
268
MarkdownOptionsPane.hardwrapsExtCheckBox.text=_Newlines in paragraph-like content as real line breaks, see
269
MarkdownOptionsPane.hardwrapsExtLink.text=Github-flavoured-Markdown
270
MarkdownOptionsPane.quotesExtCheckBox.text=Beautify single _quotes, double quotes and double angle quotes (\u00ab and \u00bb)
271
MarkdownOptionsPane.relaxedHrRulesExtCheckBox.text=Allow horizontal rules without a blank line following them
272
MarkdownOptionsPane.smartsExtCheckBox.text=Beautify apostrophes, _ellipses ("..." and ". . .") and dashes ("--" and "---")
273
MarkdownOptionsPane.strikethroughExtCheckBox.text=_Strikethrough
274
MarkdownOptionsPane.suppressHtmlBlocksExtCheckBox.text=Suppress the _output of HTML blocks
275
MarkdownOptionsPane.suppressInlineHtmlExtCheckBox.text=Suppress the o_utput of inline HTML elements
276
MarkdownOptionsPane.tablesExtCheckBox.text=_Tables similar to
277
MarkdownOptionsPane.tablesExtLabel.text=(like
278
MarkdownOptionsPane.tablesExtLabel2.text=tables, but with colspan support)
279
MarkdownOptionsPane.tablesExtLink.text=MultiMarkdown
280
MarkdownOptionsPane.tablesExtLink2.text=Markdown Extra
281
MarkdownOptionsPane.taskListItemsExtCheckBox.text=GitHub style task list items
282
MarkdownOptionsPane.wikilinksExtCheckBox.text=_Wiki-style links ("[[wiki link]]")
1283
A src/main/resources/com/scrivenvar/preview/webview.css
1
/*
2
This software is released under the MIT license:
3
4
Permission is hereby granted, free of charge, to any person obtaining a copy of
5
this software and associated documentation files (the "Software"), to deal in
6
the Software without restriction, including without limitation the rights to
7
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
the Software, and to permit persons to whom the Software is furnished to do so,
9
subject to the following conditions:
10
11
The above copyright notice and this permission notice shall be included in all
12
copies or substantial portions of the Software.
13
14
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
*/
21
22
/* Source: https://github.com/nicolashery/markdownpad-github */
23
24
/*  GitHub stylesheet for MarkdownPad (http://markdownpad.com) */
25
26
/* RESET
27
=============================================================================*/
28
29
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6,
30
p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn,
31
em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var,
32
b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label,
33
legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas,
34
details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output,
35
ruby, section, summary, time, mark, audio, video {
36
  margin: 0;
37
  padding: 0;
38
  border: 0;
39
}
40
41
/* BODY
42
=============================================================================*/
43
44
body {
45
  font-family: Helvetica, arial, freesans, clean, sans-serif;
46
  font-size: 14px;
47
  line-height: 1.6;
48
  color: #333;
49
  background-color: #fff;
50
  padding: 20px;
51
  max-width: 960px;
52
  margin: 0 auto;
53
}
54
55
body>*:first-child {
56
  margin-top: 0 !important;
57
}
58
59
body>*:last-child {
60
  margin-bottom: 0 !important;
61
}
62
63
/* BLOCKS
64
=============================================================================*/
65
66
p, blockquote, ul, ol, dl, table, pre {
67
  margin: 15px 0;
68
}
69
70
/* HEADERS
71
=============================================================================*/
72
73
h1, h2, h3, h4, h5, h6 {
74
  margin: 20px 0 10px;
75
  padding: 0;
76
  font-weight: bold;
77
  -webkit-font-smoothing: antialiased;
78
}
79
80
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
81
h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
82
  font-size: inherit;
83
}
84
85
h1 {
86
  font-size: 28px;
87
  color: #000;
88
}
89
90
h2 {
91
  font-size: 24px;
92
  border-bottom: 1px solid #ccc;
93
  color: #000;
94
}
95
96
h3 {
97
  font-size: 18px;
98
}
99
100
h4 {
101
  font-size: 16px;
102
}
103
104
h5 {
105
  font-size: 14px;
106
}
107
108
h6 {
109
  color: #777;
110
  font-size: 14px;
111
}
112
113
body>h2:first-child, body>h1:first-child, body>h1:first-child+h2,
114
body>h3:first-child, body>h4:first-child, body>h5:first-child,
115
body>h6:first-child {
116
  margin-top: 0;
117
  padding-top: 0;
118
}
119
120
a:first-child h1, a:first-child h2, a:first-child h3,
121
a:first-child h4, a:first-child h5, a:first-child h6 {
122
  margin-top: 0;
123
  padding-top: 0;
124
}
125
126
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
127
  margin-top: 10px;
128
}
129
130
/* LINKS
131
=============================================================================*/
132
133
a {
134
  color: #4183C4;
135
  text-decoration: none;
136
}
137
138
a:hover {
139
  text-decoration: underline;
140
}
141
142
/* LISTS
143
=============================================================================*/
144
145
ul, ol {
146
  padding-left: 30px;
147
}
148
149
ul li > :first-child, 
150
ol li > :first-child, 
151
ul li ul:first-of-type, 
152
ol li ol:first-of-type, 
153
ul li ol:first-of-type, 
154
ol li ul:first-of-type {
155
  margin-top: 0px;
156
}
157
158
ul ul, ul ol, ol ol, ol ul {
159
  margin-bottom: 0;
160
}
161
162
dl {
163
  padding: 0;
164
}
165
166
dl dt {
167
  font-size: 14px;
168
  font-weight: bold;
169
  font-style: italic;
170
  padding: 0;
171
  margin: 15px 0 5px;
172
}
173
174
dl dt:first-child {
175
  padding: 0;
176
}
177
178
dl dt>:first-child {
179
  margin-top: 0px;
180
}
181
182
dl dt>:last-child {
183
  margin-bottom: 0px;
184
}
185
186
dl dd {
187
  margin: 0 0 15px;
188
  padding: 0 15px;
189
}
190
191
dl dd>:first-child {
192
  margin-top: 0px;
193
}
194
195
dl dd>:last-child {
196
  margin-bottom: 0px;
197
}
198
199
/* CODE
200
=============================================================================*/
201
202
pre, code, tt {
203
  font-size: 12px;
204
  font-family: Consolas, "Liberation Mono", Courier, monospace;
205
}
206
207
code, tt {
208
  margin: 0 0px;
209
  padding: 0px 0px;
210
  white-space: nowrap;
211
  border: 1px solid #eaeaea;
212
  background-color: #f8f8f8;
213
  border-radius: 3px;
214
}
215
216
pre>code {
217
  margin: 0;
218
  padding: 0;
219
  white-space: pre;
220
  border: none;
221
  background: transparent;
222
}
223
224
pre {
225
  background-color: #f8f8f8;
226
  border: 1px solid #ccc;
227
  font-size: 13px;
228
  line-height: 19px;
229
  overflow: auto;
230
  padding: 6px 10px;
231
  border-radius: 3px;
232
}
233
234
pre code, pre tt {
235
  background-color: transparent;
236
  border: none;
237
}
238
239
kbd {
240
  -moz-border-bottom-colors: none;
241
  -moz-border-left-colors: none;
242
  -moz-border-right-colors: none;
243
  -moz-border-top-colors: none;
244
  background-color: #DDDDDD;
245
  background-image: linear-gradient(#F1F1F1, #DDDDDD);
246
  background-repeat: repeat-x;
247
  border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
248
  border-image: none;
249
  border-radius: 2px 2px 2px 2px;
250
  border-style: solid;
251
  border-width: 1px;
252
  font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
253
  line-height: 10px;
254
  padding: 1px 4px;
255
}
256
257
/* QUOTES
258
=============================================================================*/
259
260
blockquote {
261
  border-left: 4px solid #DDD;
262
  padding: 0 15px;
263
  color: #777;
264
}
265
266
blockquote>:first-child {
267
  margin-top: 0px;
268
}
269
270
blockquote>:last-child {
271
  margin-bottom: 0px;
272
}
273
274
/* HORIZONTAL RULES
275
=============================================================================*/
276
277
hr {
278
  clear: both;
279
  margin: 15px 0;
280
  height: 0px;
281
  overflow: hidden;
282
  border: none;
283
  background: transparent;
284
  border-bottom: 4px solid #ddd;
285
  padding: 0;
286
}
287
288
/* TABLES
289
=============================================================================*/
290
291
table th {
292
  font-weight: bold;
293
}
294
295
table th, table td {
296
  border: 1px solid #ccc;
297
  padding: 6px 13px;
298
}
299
300
table tr {
301
  border-top: 1px solid #ccc;
302
  background-color: #fff;
303
}
304
305
table tr:nth-child(2n) {
306
  background-color: #f8f8f8;
307
}
308
309
/* IMAGES
310
=============================================================================*/
311
312
img {
313
  max-width: 100%
314
}
315
316
/* CARET 
317
=============================================================================*/
318
319
#CARETPOSITION {
320
  border-top: 2px solid #333;
321
  border-bottom: 2px solid #333;
322
  border-right: 1px solid #333;
323
  margin-right:-1px;
324
  animation: blink 1s linear infinite;
325
}
326
327
@keyframes blink {
328
  from {
329
    visibility:hidden;
330
  }
331
  50% {
332
    visibility:hidden;
333
  }
334
  to {
335
    visibility:visible;
336
  }
337
}
1338
A src/main/resources/com/scrivenvar/scene.css
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
/*---- toolbar ----*/
29
30
.tool-bar {
31
	-fx-spacing: 0;
32
}
33
34
.tool-bar .button {
35
	-fx-background-color: transparent;
36
}
37
38
.tool-bar .button:hover {
39
	-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
40
	-fx-color: -fx-hover-base;
41
}
42
43
.tool-bar .button:armed {
44
	-fx-color: -fx-pressed-base;
45
}
146
A src/main/resources/com/scrivenvar/settings.properties
1
# ########################################################################
2
#
3
# Application
4
#
5
# ########################################################################
6
7
application.title=scrivenvar
8
application.package=com/${application.title}
9
application.messages= com.${application.title}.messages
10
11
# Suppress multiple file modified notifications for one logical modification.
12
# Given in milliseconds.
13
application.watchdog.timeout=50
14
15
# ########################################################################
16
#
17
# Preferences
18
#
19
# ########################################################################
20
21
preferences.root=com.${application.title}
22
preferences.root.state=state
23
preferences.root.options=options
24
preferences.root.definition.source=definition.source
25
26
# ########################################################################
27
#
28
# File References
29
#
30
# ########################################################################
31
32
file.stylesheet.scene=${application.package}/scene.css
33
file.stylesheet.markdown=${application.package}/editor/markdown.css
34
file.stylesheet.preview=webview.css
35
file.stylesheet.xml=${application.package}/xml.css
36
37
file.logo.16 =${application.package}/logo16.png
38
file.logo.32 =${application.package}/logo32.png
39
file.logo.128=${application.package}/logo128.png
40
file.logo.256=${application.package}/logo256.png
41
file.logo.512=${application.package}/logo512.png
42
43
# Startup script for R
44
file.r.startup=/${application.package}/startup.R
45
46
# ########################################################################
47
#
48
# Caret token
49
#
50
# ########################################################################
51
caret.token.base=CARETPOSITION
52
caret.token.markdown=%${constant.caret.token.base}%
53
caret.token.html=<span id="${caret.token.base}"></span>
54
55
# ########################################################################
56
#
57
# Filename Extensions
58
#
59
# ########################################################################
60
61
# Comma-separated list of definition filename extensions.
62
definition.file.ext.json=*.json
63
definition.file.ext.toml=*.toml
64
definition.file.ext.yaml=*.yml,*.yaml
65
definition.file.ext.properties=*.properties,*.props
66
67
# Comma-separated list of filename extensions.
68
file.ext.rmarkdown=*.Rmd
69
file.ext.rxml=*.Rxml
70
file.ext.markdown=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown}
71
file.ext.definition=${definition.file.ext.yaml}
72
file.ext.xml=*.xml,${file.ext.rxml}
73
file.ext.all=*.*
74
75
# ########################################################################
76
#
77
# Variable Name Editor
78
#
79
# ########################################################################
80
81
# Maximum number of characters for a variable name. A variable is defined
82
# as one or more non-whitespace characters up to this maximum length.
83
editor.variable.maxLength=256
84
85
# ########################################################################
86
#
87
# Dialog Preferences
88
#
89
# ########################################################################
90
91
# docs.oracle.com/javase/8/javafx/api/javafx/scene/control/ButtonBar.html
92
dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
93
dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
94
dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
95
96
# Ensures a consistent button order for alert dialogs across platforms (because
97
# the default button order on Linux defies all logic). Power to the people.
98
dialog.alert.button.order=${dialog.alert.button.order.windows}
199
A src/main/resources/com/scrivenvar/variables.yaml
1
---
2
c:
3
  protagonist:
4
    name:
5
      First: Chloe
6
      First_pos: $c.protagonist.name.First$'s
7
      Middle: Irene
8
      Family: Angelos
9
      nick:
10
        Father: Savant
11
        Mother: Sweetie
12
    colour:
13
      eyes: green
14
      hair: dark auburn
15
      syn_1: black
16
      syn_2: purple
17
      syn_11: teal
18
      syn_6: silver
19
      favourite: emerald green
20
    speech:
21
      tic: oh
22
    father:
23
      heritage: Greek
24
      name:
25
        Short: Bryce
26
        First: Bryson
27
        First_pos: $c.protagonist.father.name.First$'s
28
        Honourific: Mr.
29
      education: Masters
30
      vocation:
31
        name: robotics
32
        title: roboticist
33
      employer:
34
        name:
35
          Short: Rabota
36
          Full: $c.protagonist.father.employer.name.Short$ Designs
37
      hair:
38
        style: thick, curly
39
        colour: black
40
      eyes:
41
        colour: dark brown
42
      Endear: Dad
43
      vehicle: coupé
44
    mother:
45
      name:
46
        Short: Cass
47
        First: Cassandra
48
        First_pos: $c.protagonist.mother.name.First$'s
49
        Honourific: Mrs.
50
      education: PhD
51
      speech:
52
        tic: cute
53
        Honorific: Doctor
54
      vocation:
55
        article: an
56
        name: oceanography
57
        title: oceanographer
58
      employer:
59
        name:
60
          Full: Oregon State University
61
          Short: OSU
62
      eyes:
63
        colour: blue
64
      hair:
65
        style: thick, curly
66
        colour: dark brown
67
      Endear: Mom
68
      Endear_pos: Mom's
69
    uncle:
70
      name:
71
        First: Damian
72
        First_pos: $c.protagonist.uncle.name.First$'s
73
        Family: Moros
74
      hands:
75
        fingers:
76
          shape: long, bony
77
    friend:
78
      primary:
79
        name:
80
          First: Gerard
81
          First_pos: $c.protagonist.friend.primary.name.First$'s
82
          Family: Baran
83
          Family_pos: $c.protagonist.friend.primary.name.Family$'s
84
        favourite:
85
          colour: midnight blue
86
        eyes:
87
          colour: hazel
88
        mother:
89
          name:
90
            First: Isabella
91
            Short: Izzy
92
            Honourific: Mrs.
93
        father:
94
          name:
95
            Short: Mo
96
            First: Montgomery
97
            First_pos: $c.protagonist.friend.primary.father.name.First$'s
98
            Honourific: Mr.
99
          speech:
100
            tic: y'know
101
          endear: Pops
102
  military:
103
    primary:
104
      name:
105
        First: Felix
106
        Family: LeMay
107
        Family_pos: LeMay's
108
      rank:
109
        Short: General
110
        Full: Brigadier $c.military.primary.rank.Short$
111
      colour:
112
        eyes: gray
113
        hair: dirty brown
114
    secondary:
115
      name:
116
        Family: Grell
117
      rank: Colonel
118
      colour:
119
        eyes: green
120
        hair: deep red
121
    quaternary:
122
      name:
123
        First: Gretchen
124
        Family: Steinherz
125
  minor:
126
    primary:
127
      name:
128
        First: River
129
        Family: Banks
130
        Honourific: Mx.
131
      vocation:
132
        title: salesperson
133
      employer:
134
        Name: Geophysical Prospecting Incorporated
135
        Abbr: GPI
136
        Area: Cold Spring Creek
137
        payment: twenty million
138
    secondary:
139
      name:
140
        First: Renato
141
        Middle: Carroña
142
        Family: Salvatierra
143
        Family_pos: $c.minor.secondary.name.Family$'s
144
        Full: $c.minor.secondary.name.First$ $c.minor.secondary.name.Middle$ Alejandro Gregorio Eduardo Salomón Vidal $c.minor.secondary.name.Family$
145
        Honourific: Mister
146
        Honourific_sp: Señor
147
      vocation:
148
        title: detective
149
    tertiary:
150
      name:
151
        First: Robert
152
        Family: Hanssen
153
154
  ai:
155
    protagonist:
156
      name:
157
        first: yoky
158
        First: Yoky
159
        First_pos: $c.ai.protagonist.name.First$'s
160
        Family: Tsukuda
161
        id: 46692
162
      persona:
163
        name:
164
          First: Hoshi
165
          First_pos: $c.ai.protagonist.persona.name.First$'s
166
          Family: Yamamoto
167
          Family_pos: $c.ai.protagonist.persona.name.Family$'s
168
      culture: Japanese-American
169
      ethnicity: Asian
170
      rank: Technical Sergeant
171
      speech:
172
        tic: okay
173
    first:
174
      Name: Prôtos
175
      Name_pos: Prôtos'
176
      age:
177
        actual: twenty-six weeks
178
        virtual: five years
179
    second:
180
      Name: Défteros
181
    third:
182
      Name: Trítos
183
    fourth:
184
      Name: Tétartos
185
    material:
186
      type: metal
187
      raw: ilmenite
188
      extract: ore
189
      name:
190
        short: titanium
191
        long: $c.ai.material.name.short$ dioxide
192
        Abbr: TiO~2~
193
      pejorative: tin
194
  animal:
195
    protagonist:
196
      Name: Trufflers
197
      type: pig
198
    antagonist:
199
      name: coywolf
200
      Name: Coywolf
201
      plural: coywolves
202
203
narrator:
204
  one: (by $c.protagonist.father.name.First$ $c.protagonist.name.Family$)
205
  two: (by $c.protagonist.mother.name.First$ $c.protagonist.name.Family$)
206
207
military:
208
  name:
209
    Short: Agency
210
    Short_pos: $military.name.Short$'s
211
    plural: agencies
212
  machine:
213
    Name: Skopós
214
    Name_pos: $military.machine.Name$'
215
    Location: Arctic
216
    predictor: quantum chips
217
  land:
218
    name:
219
      Full: $military.name.Short$ of Defence
220
    Slogan: Safety in Numbers
221
  air:
222
    name:
223
      Full: $military.name.Short$ of Air
224
  compound:
225
    type: base
226
    lights:
227
      colour: blue
228
    nick:
229
      Prefix: Catacombs
230
      prep: of
231
      Suffix: Tartarus
232
233
government:
234
  Country: United States
235
236
location:
237
  protagonist:
238
    City: Corvallis
239
    Region: Oregon
240
    Geography: Willamette Valley
241
    secondary:
242
      City: Willow Branch Spring
243
      Region: Oregon
244
      Geography: Wheeler County
245
      Water: Clarno Rapids
246
      Road: Shaniko-Fossil Highway
247
    tertiary:
248
      City: Leavenworth
249
      Region: Washington
250
      Type: Bavarian village
251
    school:
252
      address: 1400 Northwest Buchanan Avenue
253
    hospital:
254
      Name: Good Samaritan Regional Medical Center
255
  ai:
256
    escape:
257
      country:
258
        Name: Ecuador
259
        Name_pos: Ecuador's
260
      mountain:
261
        Name: Chimborazo
262
263
language:
264
  ai:
265
    article: an
266
    singular: exanimis
267
    plural: exanimēs
268
    brain:
269
      singular: superum
270
      plural: supera
271
    title: memristor array
272
    Title: Memristor Array
273
  police:
274
    slang:
275
      singular: mippo
276
      plural: $language.police.slang.singular$s
277
278
date:
279
  anchor: 2042-09-02
280
  protagonist:
281
    born: 0
282
    conceived: -243
283
    attacked:
284
      first: 2192
285
      second: 8064
286
    father:
287
      attacked:
288
        first: -8205
289
      date:
290
        second: -1550
291
    family:
292
      moved:
293
        first: $date.protagonist.conceived$ + 35
294
  game:
295
    played:
296
      first: $date.protagonist.born$ - 672
297
      second: $date.protagonist.family.moved.first$ + 2
298
  ai:
299
    interviewed: 6198
300
    onboarded: $date.ai.interviewed$ + 290
301
    diagnosed: $date.ai.onboarded$ + 2
302
    resigned: $date.ai.diagnosed$ + 3
303
    trapped: $date.ai.resigned$ + 26
304
    torturer: $date.ai.trapped$ + 18
305
    memristor: $date.ai.torturer$ + 61
306
    ethics: $date.ai.memristor$ + 415
307
    trained: $date.ai.ethics$ + 385
308
    mindjacked: $date.ai.trained$ + 22
309
    bombed: $date.ai.mindjacked$ + 458
310
  military:
311
    machine:
312
      Construction: Six years
313
314
plot:
315
  Log: $c.ai.protagonist.name.First_pos$ Chronicles
316
  Channel: Quantum Channel
317
318
  device:
319
    computer:
320
      Name: Tau
321
    network:
322
      Name: Internet
323
    paper:
324
      name:
325
        full: electronic sheet
326
        short: sheet
327
    typewriter:
328
      Name: Underwood
329
      year: nineteen twenties
330
      room: root cellar
331
    portable:
332
      name: nanobook
333
    vehicle:
334
      name: robocars
335
      Name: Robocars
336
    sensor:
337
      name: BMP1580
338
    phone:
339
      name: comm
340
      name_pos: $plot.device.phone.name$'s
341
      Name: Comm
342
      plural: $plot.device.phone.name$s
343
    video:
344
      name: vidfeed
345
      plural: $plot.device.video.name$s
346
    game:
347
      Name: Psynæris
348
      thought: transed
349
      machine: telecognos
350
      location:
351
        Building: Nijō Castle
352
        District: Gion
353
        City: Kyoto
354
        Country: Japan
355
356
farm:
357
  population:
358
    estimate: 350
359
    actual: 1,000
360
  energy: 9800kJ
361
  width: 55m
362
  length: 55m
363
  storeys: 10
364
365
lamp:
366
  height: 0.17m
367
  length: 1.22m
368
  width: 0.28m
369
370
crop:
371
  name: 
372
    singular: tomato
373
    plural: $crop.name.singular$es
374
  energy: 318kJ
375
  weight: 450g
376
  yield: 50
377
  harvests: 7
378
  diameter: 2m
379
  height: 1.5m
380
381
heading:
382
  ch_01: Till
383
  ch_02: Sow
384
  ch_03: Seed
385
  ch_04: Germinate
386
  ch_05: Grow
387
  ch_06: Shoot
388
  ch_07: Bud
389
  ch_08: Bloom
390
  ch_09: Pollinate
391
  ch_10: Fruit
392
  ch_11: Harvest
393
  ch_12: Deliver
394
  ch_13: Spoil
395
  ch_14: Revolt
396
  ch_15: Compost
397
  ch_16: Burn
398
  ch_17: Release
399
  ch_18: End Notes
400
  ch_19: Characters
401
402
inference:
403
  unit: per cent
404
  min: two
405
  ch_sow: eighty
406
  ch_seed: fifty-two
407
  ch_germinate: thirty-one
408
  ch_grow: fifteen
409
  ch_shoot: seven
410
  ch_bloom: four
411
  ch_pollinate: two
412
  ch_harvest: ninety-five
413
  ch_delivery: ninety-eight
414
415
link:
416
  tartarus: https://en.wikipedia.org/wiki/Tartarus
417
  exploits: https://www.google.ca/search?q=inurl:ftp+password+filetype:xls
418
  atalanta: https://en.wikipedia.org/wiki/Atalanta
419
  detain: https://goo.gl/RCNuOQ
420
  ceramics: https://en.wikipedia.org/wiki/Transparent_ceramics
421
  algernon: https://en.wikipedia.org/wiki/Flowers_for_Algernon
422
  holocaust: https://en.wikipedia.org/wiki/IBM_and_the_Holocaust
423
  memristor: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.404.9037\&rep=rep1\&type=pdf
424
  surveillance: https://www.youtube.com/watch?v=XEVlyP4_11M#t=1487
425
  tor: https://www.torproject.org
426
  hydra: https://en.wikipedia.org/wiki/Lernaean_Hydra
427
  foliage: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3691134
428
  drake: http://www.bbc.com/future/story/20120821-how-many-alien-worlds-exist
429
  fermi: https://arxiv.org/pdf/1404.0204v1.pdf
430
  face: https://www.youtube.com/watch?v=ladqJQLR2bA
431
  expenditures: http://wikipedia.org/wiki/List_of_countries_by_military_expenditures
432
  governance: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2003531
433
  asimov: https://en.wikipedia.org/wiki/Three_Laws_of_Robotics
434
  clarke: https://en.wikipedia.org/wiki/Clarke's_three_laws
435
  jetpack: http://jetpackaviation.com/
436
  hoverboard: https://www.youtube.com/watch?v=WQzLrvz4DKQ
437
  eyes_five: https://en.wikipedia.org/wiki/Five_Eyes
438
  eyes_nine: https://www.privacytools.io/
439
  eyes_fourteen: http://electrospaces.blogspot.nl/2013/12/14-eyes-are-3rd-party-partners-forming.html
440
  tourism: http://www.spacefuture.com/archive/investigation_on_the_economic_and_technological_feasibiity_of_commercial_passenger_transportation_into_leo.shtml
441
1442
A src/main/resources/com/scrivenvar/xml.css
1
.tagmark {
2
    -fx-fill: gray;
3
}
4
.anytag {
5
    -fx-fill: crimson;
6
}
7
.paren {
8
    -fx-fill: firebrick;
9
    -fx-font-weight: bold;
10
}
11
.attribute {
12
    -fx-fill: darkviolet;
13
}
14
.avalue {
15
    -fx-fill: black;
16
}
117
18
.comment {
19
	-fx-fill: teal;
20
}