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
17
*.ttf binary
18
119
A .gitignore
1
dist
2
scrivenvar.bin
3
scrivenvar.exe
4
build
5
.gradle
16
A .idea/.gitignore
1
workspace.xml
12
A .idea/codeStyles/codeStyleConfig.xml
1
1
<component name="ProjectCodeStyleConfiguration">
2
  <state>
3
    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
4
  </state>
5
</component>
A .idea/compiler.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="CompilerConfiguration">
4
    <bytecodeTargetLevel target="11" />
5
  </component>
6
</project>
A .idea/dictionaries/jarvisd.xml
1
1
<component name="ProjectDictionaryState">
2
  <dictionary name="jarvisd">
3
    <words>
4
      <w>blockquotes</w>
5
    </words>
6
  </dictionary>
7
</component>
A .idea/gradle.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="GradleMigrationSettings" migrationVersion="1" />
4
  <component name="GradleSettings">
5
    <option name="linkedExternalProjectsSettings">
6
      <GradleProjectSettings>
7
        <option name="distributionType" value="LOCAL" />
8
        <option name="externalProjectPath" value="$PROJECT_DIR$" />
9
        <option name="gradleHome" value="/opt/gradle" />
10
        <option name="gradleJvm" value="#JAVA_HOME" />
11
        <option name="modules">
12
          <set>
13
            <option value="$PROJECT_DIR$" />
14
          </set>
15
        </option>
16
      </GradleProjectSettings>
17
    </option>
18
  </component>
19
</project>
A .idea/jarRepositories.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="RemoteRepositoriesConfiguration">
4
    <remote-repository>
5
      <option name="id" value="central" />
6
      <option name="name" value="Maven Central repository" />
7
      <option name="url" value="https://repo1.maven.org/maven2" />
8
    </remote-repository>
9
    <remote-repository>
10
      <option name="id" value="jboss.community" />
11
      <option name="name" value="JBoss Community repository" />
12
      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
13
    </remote-repository>
14
    <remote-repository>
15
      <option name="id" value="MavenRepo" />
16
      <option name="name" value="MavenRepo" />
17
      <option name="url" value="https://repo.maven.apache.org/maven2/" />
18
    </remote-repository>
19
    <remote-repository>
20
      <option name="id" value="maven" />
21
      <option name="name" value="maven" />
22
      <option name="url" value="https://oss.sonatype.org/content/repositories/snapshots/" />
23
    </remote-repository>
24
    <remote-repository>
25
      <option name="id" value="BintrayJCenter" />
26
      <option name="name" value="BintrayJCenter" />
27
      <option name="url" value="https://jcenter.bintray.com/" />
28
    </remote-repository>
29
    <remote-repository>
30
      <option name="id" value="maven2" />
31
      <option name="name" value="maven2" />
32
      <option name="url" value="https://nexus.bedatadriven.com/content/groups/public" />
33
    </remote-repository>
34
  </component>
35
</project>
A .idea/misc.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="ExternalStorageConfigurationManager" enabled="true" />
4
  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="14" project-jdk-type="JavaSDK">
5
    <output url="file://$PROJECT_DIR$/build" />
6
  </component>
7
</project>
A .idea/rGraphicsSettings.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="RGraphicsSettings">
4
    <option name="height" value="1600" />
5
    <option name="resolution" value="112" />
6
    <option name="version" value="1" />
7
    <option name="width" value="2560" />
8
  </component>
9
</project>
A .idea/rSettings.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="RSettings">
4
    <option name="interpreterPath" value="/usr/bin/R" />
5
  </component>
6
</project>
A .idea/rpackages.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="RPackageService">
4
    <option name="enabledRepositoryUrls">
5
      <list>
6
        <option value="@CRAN@" />
7
      </list>
8
    </option>
9
  </component>
10
</project>
A .idea/scrivenvar.iml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<module type="JAVA_MODULE" version="4" />
A .idea/uiDesigner.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="Palette2">
4
    <group name="Swing">
5
      <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
6
        <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
7
      </item>
8
      <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
9
        <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
10
      </item>
11
      <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
12
        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
13
      </item>
14
      <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
15
        <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
16
      </item>
17
      <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
18
        <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
19
        <initial-values>
20
          <property name="text" value="Button" />
21
        </initial-values>
22
      </item>
23
      <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
24
        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
25
        <initial-values>
26
          <property name="text" value="RadioButton" />
27
        </initial-values>
28
      </item>
29
      <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
30
        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
31
        <initial-values>
32
          <property name="text" value="CheckBox" />
33
        </initial-values>
34
      </item>
35
      <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
36
        <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
37
        <initial-values>
38
          <property name="text" value="Label" />
39
        </initial-values>
40
      </item>
41
      <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
42
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
43
          <preferred-size width="150" height="-1" />
44
        </default-constraints>
45
      </item>
46
      <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
47
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
48
          <preferred-size width="150" height="-1" />
49
        </default-constraints>
50
      </item>
51
      <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
52
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
53
          <preferred-size width="150" height="-1" />
54
        </default-constraints>
55
      </item>
56
      <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
57
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
58
          <preferred-size width="150" height="50" />
59
        </default-constraints>
60
      </item>
61
      <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
62
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
63
          <preferred-size width="150" height="50" />
64
        </default-constraints>
65
      </item>
66
      <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
67
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
68
          <preferred-size width="150" height="50" />
69
        </default-constraints>
70
      </item>
71
      <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
72
        <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
73
      </item>
74
      <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
75
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
76
          <preferred-size width="150" height="50" />
77
        </default-constraints>
78
      </item>
79
      <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
80
        <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
81
          <preferred-size width="150" height="50" />
82
        </default-constraints>
83
      </item>
84
      <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
85
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
86
          <preferred-size width="150" height="50" />
87
        </default-constraints>
88
      </item>
89
      <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
90
        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
91
          <preferred-size width="200" height="200" />
92
        </default-constraints>
93
      </item>
94
      <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
95
        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
96
          <preferred-size width="200" height="200" />
97
        </default-constraints>
98
      </item>
99
      <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
100
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
101
      </item>
102
      <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
103
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
104
      </item>
105
      <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
106
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
107
      </item>
108
      <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
109
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
110
      </item>
111
      <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
112
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
113
          <preferred-size width="-1" height="20" />
114
        </default-constraints>
115
      </item>
116
      <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
117
        <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
118
      </item>
119
      <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
120
        <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
121
      </item>
122
    </group>
123
  </component>
124
</project>
A .idea/vcs.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="VcsDirectoryMappings">
4
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
  </component>
6
</project>
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 BUILD.md
1
# Introduction
2
3
This document describes how to build the application and platform binaries.
4
5
# Requirements
6
7
Download and install the following software packages:
8
9
* [OpenJDK 14](https://openjdk.java.net)
10
* [Gradle 6.4](https://gradle.org/releases)
11
12
# Build
13
14
Build the application überjar as follows:
15
16
    gradle clean jar
17
18
The application is built.
19
20
# Run
21
22
After the application is compiled, run it as follows:
23
24
    java -jar build/libs/scrivenvar.jar
25
26
On Windows:
27
28
    java -jar build\libs\scrivenvar.jar
29
30
# Installers
31
32
This section describes how to set up the development environment and
33
build native executables for supported operating systems.
34
35
## Setup
36
37
Follow these one-time setup instructions to begin:
38
39
1. Ensure `$HOME/bin` is set in the `PATH` environment variable.
40
1. Move `build-template` into `$HOME/bin`.
41
42
Setup is complete.
43
44
## Binaries
45
46
Run the `installer` script to build platform-specific binaries, such as:
47
48
    ./installer -V -o linux
49
50
The `installer` script:
51
52
* downloads a JDK;
53
* generates a run script;
54
* bundles the JDK, run script, and JAR file; and
55
* creates a standalone binary, so no installation required.
56
57
Run `./installer -h` to see all command-line options.
58
59
# Versioning
60
61
Version numbers are read directly from Git using a plugin. The version
62
number is written to `app.properties`, a properties file in the `resources`
63
directory that can be read from within the application.
64
165
A LICENSE.md
1
# License
2
3
Copyright 2020 White Magic Software, Ltd.
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.
127
A R/README.md
1
# R Functions
2
3
Import the files in this directory into the application, which include:
4
5
* pluralize.R
6
* possessive.R
7
* conversion.R
8
* csv.R
9
10
# pluralize.R
11
12
This file defines a function that implements most of Damian Conway's [An Algorithmic Approach to English Pluralization](http://blob.perl.org/tpc/1998/User_Applications/Algorithmic%20Approach%20Plurals/Algorithmic_Plurals.html).
13
14
## Usage
15
16
Example usages of the pluralize function include:
17
18
    `r#pluralize( 'mouse' )` - mice
19
    `r#pluralize( 'buzz' )` - buzzes
20
    `r#pluralize( 'bus' )` - busses
21
22
# possessive.R
23
24
This file defines a function that applies possessives to English words.
25
26
## Usage
27
28
Example usages of the possessive function include:
29
30
    `r#pos( 'Ross' )` - Ross'
31
    `r#pos( 'Ruby' )` - Ruby's
32
    `r#pos( 'Lois' )` - Lois'
33
    `r#pos( 'my' )` - mine
34
    `r#pos( 'Your' )` - Yours
35
136
A R/bootstrap.R
1
setwd( '$application.r.working.directory$' )
2
assign( "anchor", '$date.anchor$', envir = .GlobalEnv )
3
4
source( 'pluralize.R' )
5
source( 'possessive.R' )
6
source( 'conversion.R' )
7
source( 'csv.R' )
8
19
A R/conversion.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# Substitute R expressions in a document with their evaluated value. The
26
# anchor variable must be set for functions that use relative dates.
27
# -----------------------------------------------------------------------------
28
29
# -----------------------------------------------------------------------------
30
# Evaluates an expression; writes s if there is no expression.
31
# -----------------------------------------------------------------------------
32
x <- function( s ) {
33
  tryCatch( {
34
    r = eval( parse( text = s ) )
35
36
    # If the result isn't primitive, then it was probably parsed into
37
    # an unprintable object (e.g., "gray" becomes a colour). In those
38
    # cases, return the original text string. Otherwise, an atomic
39
    # value means a primitive type (string, integer, etc.) that can be
40
    # written directly into the document.
41
    ifelse( is.atomic( r ), r, s );
42
  },
43
  warning = function( w ) { s },
44
  error = function( e ) { s } )
45
}
46
47
# -----------------------------------------------------------------------------
48
# Returns a date offset by a given number of days, relative to the given
49
# date (d). This does not use the anchor, but is used to get the anchor's
50
# value as a date.
51
# -----------------------------------------------------------------------------
52
when <- function( d, n = 0, format = "%Y-%m-%d" ) {
53
  as.Date( d, format = format ) + x( n )
54
}
55
56
# -----------------------------------------------------------------------------
57
# Full date (s) offset by an optional number of days before or after.
58
# This will remove leading zeros (applying leading spaces instead, which
59
# are ignored by any worthwhile typesetting engine).
60
# -----------------------------------------------------------------------------
61
annal <- function( days = 0, format = "%Y-%m-%d", oformat = "%B %d, %Y" ) {
62
  format( when( anchor, days ), format = oformat )
63
}
64
65
# -----------------------------------------------------------------------------
66
# Extracts the year from a date string.
67
# -----------------------------------------------------------------------------
68
year <- function( days = 0, format = "%Y-%m-%d" ) {
69
  annal( days, format, "%Y" )
70
}
71
72
# -----------------------------------------------------------------------------
73
# Day of the week (in days since the anchor date).
74
# -----------------------------------------------------------------------------
75
weekday <- function( n ) {
76
  weekdays( when( anchor, n ) )
77
}
78
79
# -----------------------------------------------------------------------------
80
# String concatenate function alias because paste0 is a terrible name.
81
# -----------------------------------------------------------------------------
82
concat <- paste0
83
84
# -----------------------------------------------------------------------------
85
# Translates a number from digits to words using Chicago Manual of Style.
86
# This does not translate numbers greater than one hundred. If ordinal
87
# is TRUE, this will return the ordinal name. This will not produce ordinals
88
# for numbers greater than 100.
89
# -----------------------------------------------------------------------------
90
cms <- function( n, ordinal = FALSE ) {
91
  n <- x( n )
92
93
  if( n == 0 ) {
94
    if( ordinal ) {
95
      return( "zeroth" )
96
    }
97
98
    return( "zero" )
99
  }
100
101
  # Concatenate this a little later.
102
  if( n < 0 ) {
103
    result = "negative "
104
    n = abs( n )
105
  }
106
107
  # Do not spell out numbers greater than one hundred.
108
  if( n > 100 ) {
109
    # Comma-separated numbers.
110
    return( commas( n ) )
111
  }
112
113
  # Don't go beyond 100.
114
  if( n == 100 ) {
115
    if( ordinal ) {
116
      return( "one hundredth" )
117
    }
118
119
    return( "one hundred" )
120
  }
121
122
  # Samuel Langhorne Clemens noted English has too many exceptions.
123
  small = c(
124
    "one", "two", "three", "four", "five",
125
    "six", "seven", "eight", "nine", "ten",
126
    "eleven", "twelve", "thirteen", "fourteen", "fifteen",
127
    "sixteen", "seventeen", "eighteen", "nineteen"
128
  )
129
130
  ord_small = c(
131
    "first", "second", "third", "fourth", "fifth",
132
    "sixth", "seventh", "eighth", "ninth", "tenth",
133
    "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth",
134
    "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth"
135
  )
136
137
  # After this, the number (n) is between 20 and 99.
138
  if( n < 20 ) {
139
    if( ordinal ) {
140
      return( .subset( ord_small, n %% 100 ) )
141
    }
142
143
    return( .subset( small, n %% 100 ) )
144
  }
145
146
  tens = c( "",
147
    "twenty", "thirty", "forty", "fifty",
148
    "sixty", "seventy", "eighty", "ninety"
149
  )
150
151
  ord_tens = c( "",
152
    "twentieth", "thirtieth", "fortieth", "fiftieth",
153
    "sixtieth", "seventieth", "eightieth", "ninetieth"
154
  )
155
156
  ones_index = n %% 10
157
  n = n %/% 10
158
159
  # No number in the ones column, so the number must be a multiple of ten.
160
  if( ones_index == 0 ) {
161
    if( ordinal ) {
162
      return( .subset( ord_tens, n ) )
163
    }
164
165
    return( .subset( tens, n ) )
166
  }
167
168
  # Find the value from the ones column.
169
  if( ordinal ) {
170
    unit_1 = .subset( ord_small, ones_index )
171
  }
172
  else {
173
    unit_1 = .subset( small, ones_index )
174
  }
175
176
  # Find the tens column.
177
  unit_10 = .subset( tens, n )
178
179
  # Hyphenate the tens and the ones together.
180
  concat( unit_10, concat( "-", unit_1 ) )
181
}
182
183
# -----------------------------------------------------------------------------
184
# Returns a number as a comma-delimited string. This is a work-around
185
# until Renjin fixes https://github.com/bedatadriven/renjin/issues/338
186
# -----------------------------------------------------------------------------
187
commas <- function( n ) {
188
  n <- x( n )
189
190
  s <- sprintf( "%03.0f", n %% 1000 )
191
  n <- n %/% 1000
192
193
  while( n > 0 ) {
194
    s <- concat( sprintf( "%03.0f", n %% 1000 ), ',', s )
195
    n <- n %/% 1000
196
  }
197
198
  gsub( '^0*', '', s )
199
}
200
201
# -----------------------------------------------------------------------------
202
# Returns a human-readable string that provides the elapsed time between
203
# two numbers in terms of years, months, and days. If any unit value is zero,
204
# the unit is not included. The words (year, month, day) are pluralised
205
# according to English grammar. The numbers are written out according to
206
# Chicago Manual of Style. This applies the serial comma.
207
#
208
# Both numbers are offsets relative to the anchor date.
209
#
210
# If all unit values are zero, this returns s ("same day" by default).
211
#
212
# If the start date (began) is greater than end date (ended), the dates are
213
# swapped before calculations are performed. This allows any two dates
214
# to be compared and positive unit values are always returned.
215
# -----------------------------------------------------------------------------
216
elapsed <- function( began, ended, s = "same day" ) {
217
  began = when( anchor, began )
218
  ended = when( anchor, ended )
219
220
  # Swap the dates if the end date comes before the start date.
221
  if( as.integer( ended - began ) < 0 ) {
222
    tempd = began
223
    began = ended
224
    ended = tempd
225
  }
226
227
  # Calculate number of elapsed years.
228
  years = length( seq( from = began, to = ended, by = 'year' ) ) - 1
229
230
  # Move the start date up by the number of elapsed years.
231
  if( years > 0 ) {
232
    began = seq( began, length = 2, by = concat( years, " years" ) )[2]
233
    years = pl.numeric( "year", years )
234
  }
235
  else {
236
    # Zero years.
237
    years = ""
238
  }
239
240
  # Calculate number of elapsed months, excluding years.
241
  months = length( seq( from = began, to = ended, by = 'month' ) ) - 1
242
243
  # Move the start date up by the number of elapsed months
244
  if( months > 0 ) {
245
    began = seq( began, length = 2, by = concat( months, " months" ) )[2]
246
    months = pl.numeric( "month", months )
247
  }
248
  else {
249
    # Zero months
250
    months = ""
251
  }
252
253
  # Calculate number of elapsed days, excluding months and years.
254
  days = length( seq( from = began, to = ended, by = 'day' ) ) - 1
255
256
  if( days > 0 ) {
257
    days = pl.numeric( "day", days )
258
  }
259
  else {
260
    # Zero days
261
    days = ""
262
  }
263
264
  if( years <= 0 && months <= 0 && days <= 0 ) {
265
    return( s )
266
  }
267
268
  # Put them all in a vector, then remove the empty values.
269
  s <- c( years, months, days )
270
  s <- s[ s != "" ]
271
272
  r <- paste( s, collapse = ", " )
273
274
  # If all three items are present, replace the last comma with ", and".
275
  if( length( s ) > 2 ) {
276
    return( gsub( "(.*),", "\\1, and", r ) )
277
  }
278
279
  # Does nothing if no commas are present.
280
  gsub( "(.*),", "\\1 and", r )
281
}
282
283
# -----------------------------------------------------------------------------
284
# Returns the number (n) in English followed by the plural or singular
285
# form of the given string (s; resumably a noun), if applicable, according
286
# to English grammar. That is, pl.numeric( "wolf", 5 ) will return
287
# "five wolves".
288
# -----------------------------------------------------------------------------
289
pl.numeric <- function( s, n ) {
290
  concat( cms( n ), concat( " ", pluralise( s, n ) ) )
291
}
292
293
# -----------------------------------------------------------------------------
294
# Pluralise s if n is not equal to 1.
295
# -----------------------------------------------------------------------------
296
pl <- function( s, n=2 ) {
297
  pluralize( s, x( n ) )
298
}
299
300
# -----------------------------------------------------------------------------
301
# Name of the season, starting with an capital letter.
302
# -----------------------------------------------------------------------------
303
season <- function( n, format = "%Y-%m-%d" ) {
304
  WS <- as.Date("2016-12-15", "%Y-%m-%d") # Winter Solstice
305
  SE <- as.Date("2016-03-15", "%Y-%m-%d") # Spring Equinox
306
  SS <- as.Date("2016-06-15", "%Y-%m-%d") # Summer Solstice
307
  AE <- as.Date("2016-09-15", "%Y-%m-%d") # Autumn Equinox
308
309
  d <- when( anchor, n )
310
  d <- as.Date( strftime( d, format="2016-%m-%d" ) )
311
312
  ifelse( d >= WS | d < SE, "Winter",
313
    ifelse( d >= SE & d < SS, "Spring",
314
      ifelse( d >= SS & d < AE, "Summer", "Autumn" )
315
    )
316
  )
317
}
318
319
# -----------------------------------------------------------------------------
320
# Converts the first letter in a string to lowercase
321
# -----------------------------------------------------------------------------
322
lc <- function( s ) {
323
  concat( tolower( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
324
}
325
326
# -----------------------------------------------------------------------------
327
# Converts the entire string to lowercase
328
# -----------------------------------------------------------------------------
329
lower <- tolower
330
331
# -----------------------------------------------------------------------------
332
# Converts the first letter in a string to uppercase
333
# -----------------------------------------------------------------------------
334
uc <- function( s ) {
335
  concat( toupper( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
336
}
337
338
# -----------------------------------------------------------------------------
339
# Returns the number of days between the given dates.
340
# -----------------------------------------------------------------------------
341
days <- function( d1, d2, format = "%Y-%m-%d" ) {
342
  dates = c( d1, d2 )
343
  dt = strptime( dates, format = format )
344
  as.integer( difftime( dates[2], dates[1], units = "days" ) )
345
}
346
347
# -----------------------------------------------------------------------------
348
# Returns the number of years elapsed.
349
# -----------------------------------------------------------------------------
350
years <- function( began, ended ) {
351
  began = when( anchor, began )
352
  ended = when( anchor, ended )
353
354
  # Swap the dates if the end date comes before the start date.
355
  if( as.integer( ended - began ) < 0 ) {
356
    tempd = began
357
    began = ended
358
    ended = tempd
359
  }
360
361
  # Calculate number of elapsed years.
362
  length( seq( from = began, to = ended, by = 'year' ) ) - 1
363
}
364
365
# -----------------------------------------------------------------------------
366
# Full name of the month, starting with a capital letter.
367
# -----------------------------------------------------------------------------
368
month <- function( n ) {
369
  # Faster than month.name[ x( n ) ]
370
  .subset( month.name, x( n ) )
371
}
372
373
# -----------------------------------------------------------------------------
374
# -----------------------------------------------------------------------------
375
money <- function( n ) {
376
  commas( x( n ) )
377
}
378
379
# -----------------------------------------------------------------------------
380
# -----------------------------------------------------------------------------
381
timeline <- function( n ) {
382
  concat( weekday( n ), ", ", annal( n ), " (", season( n ), ")" )
383
}
384
385
# -----------------------------------------------------------------------------
386
# Rounds to the nearest base value (e.g., round to nearest 10).
387
#
388
# @param base The nearest value to round to.
389
# -----------------------------------------------------------------------------
390
round.up <- function( n, base = 5 ) {
391
  base * round( x( n ) / base )
392
}
393
394
# -----------------------------------------------------------------------------
395
# Computes linear distance between two points using Haversine formula.
396
# Although Earth is an oblate spheroid, this will produce results close
397
# enough for most purposes.
398
#
399
# @param lat1/lon1 The source latitude and longitude.
400
# @param lat2/lon2 The destination latitude and longitude.
401
# @param radius The radius of the sphere.
402
#
403
# @return The distance between the two coordinates in meters.
404
# -----------------------------------------------------------------------------
405
haversine <- function( lat1, lon1, lat2, lon2, radius = 6371 ) {
406
  # Convert decimal degrees to radians
407
  lon1 = lon1 * pi / 180
408
  lon2 = lon2 * pi / 180
409
  lat1 = lat1 * pi / 180
410
  lat2 = lat2 * pi / 180
411
412
  # Haversine formula
413
  dlon = lon2 - lon1
414
  dlat = lat2 - lat1
415
  a = sin( dlat / 2 ) ** 2 + cos( lat1 ) * cos( lat2 ) * sin( dlon / 2 ) ** 2
416
  c = 2 * atan2( sqrt( a ), sqrt( 1-a ) )
417
418
  return( radius * c * 1000 )
419
}
420
1421
A R/csv.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# Converts CSV to Markdown.
26
#
27
# Reads a CSV file and converts the contents to a Markdown table. The
28
# file must be in the working directory as specified by setwd.
29
#
30
# @param f The filename to convert.
31
# @param decimals Rounded decimal places (default 1).
32
# @param totals Include total sums (default TRUE).
33
# @param align Right-align numbers (default TRUE).
34
# -----------------------------------------------------------------------------
35
csv2md <- function( f, decimals = 2, totals = T, align = T ) {
36
  # Read the CVS data from the file; ensure strings become characters.
37
  df <- read.table( f, sep=',', header=T, stringsAsFactors=F )
38
39
  if( totals ) {
40
    # Determine what columns can be summed.
41
    number <- which( unlist( lapply( df, is.numeric ) ) )
42
43
    # Use colSums when more than one summable column exists.
44
    if( length( number ) > 1 ) {
45
      f.sum <- colSums
46
    }
47
    else {
48
      f.sum <- sum
49
    }
50
51
    # Calculate the sum of all the summable columns and insert the
52
    # results back into the data frame.
53
    df[ (nrow( df ) + 1), number ] <- f.sum( df[, number], na.rm=TRUE )
54
55
    # pluralise would be heavyweight here.
56
    if( length( number ) > 1 ) {
57
      t <- "**Totals**"
58
    }
59
    else {
60
      t <- "**Total**"
61
    }
62
63
    # Change the first column of the last line to "Total(s)".
64
    df[ nrow( df ), 1 ] <- t
65
66
    # Don't clutter the output with "NA" text.
67
    df[ is.na( df ) ] <- ""
68
  }
69
70
  if( align ) {
71
    is.char <- vapply( df, is.character, logical( 1 ) )
72
    dashes <- paste( ifelse( is.char, ':---', '---:' ), collapse='|' )
73
  }
74
  else {
75
    dashes <- paste( rep( '---', length( df ) ), collapse = '|' )
76
  }
77
78
  # Create a Markdown version of the data frame.
79
  paste(
80
    paste( names( df ), collapse = '|'), '\n',
81
    dashes, '\n', 
82
    paste(
83
      Reduce( function( x, y ) {
84
          paste( x, format( y, digits = decimals ), sep = '|' )
85
        }, df
86
      ),
87
      collapse = '|\n', sep=''
88
    )
89
  )
90
}
91
192
A R/pluralize.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# See Damian Conway's "An Algorithmic Approach to English Pluralization":
26
#   http://goo.gl/oRL4MP
27
# See Oliver Glerke's Evo Inflector: https://github.com/atteo/evo-inflector/
28
# See Shevek's Pluralizer: https://github.com/shevek/linguistics/
29
# See also: http://www.freevectors.net/assets/files/plural.txt
30
# -----------------------------------------------------------------------------
31
pluralize <- function( s, n ) {
32
  result <- s
33
34
  # Partial implementation of Conway's algorithm for nouns.
35
  if( n != 1 ) {
36
    if( pl.noninflective( s ) ||
37
        pl.suffix( "es", s ) ||
38
        pl.suffix( "fish", s ) ||
39
        pl.suffix( "ois", s ) ||
40
        pl.suffix( "sheep", s ) ||
41
        pl.suffix( "deer", s ) ||
42
        pl.suffix( "pox", s ) ||
43
        pl.suffix( "[A-Z].*ese", s ) ||
44
        pl.suffix( "itis", s ) ) {
45
      # 1. Retain non-inflective user-mapped noun as is.
46
      # 2. Retain non-inflective plural as is.
47
      result <- s
48
    }
49
    else if( pl.is.irregular.pl( s ) ) {
50
      # 4. Change irregular plurals based on mapping.
51
      result <- pl.irregular.pl( s )
52
    }
53
    else if( pl.is.irregular.es( s ) ) {
54
      # x. From Shevek's
55
      result <- pl.inflect( s, "", "es" )
56
    }
57
    else if( pl.suffix( "man", s ) ) {
58
      # 5. For -man, change -an to -en
59
      result <- pl.inflect( s, "an", "en" )
60
    }
61
    else if( pl.suffix( "[lm]ouse", s ) ) {
62
      # 5. For [lm]ouse, change -ouse to -ice
63
      result <- pl.inflect( s, "ouse", "ice" )
64
    }
65
    else if( pl.suffix( "tooth", s ) ) {
66
      # 5. For -tooth, change -ooth to -eeth
67
      result <- pl.inflect( s, "ooth", "eeth" )
68
    }
69
    else if( pl.suffix( "goose", s ) ) {
70
      # 5. For -goose, change -oose to -eese
71
      result <- pl.inflect( s, "oose", "eese" )
72
    }
73
    else if( pl.suffix( "foot", s ) ) {
74
      # 5. For -foot, change -oot to -eet
75
      result <- pl.inflect( s, "oot", "eet" )
76
    }
77
    else if( pl.suffix( "zoon", s ) ) {
78
      # 5. For -zoon, change -on to -a
79
      result <- pl.inflect( s, "on", "a" )
80
    }
81
    else if( pl.suffix( "[csx]is", s ) ) {
82
      # 5. Change -cis, -sis, -xis to -es
83
      result <- pl.inflect( s, "is", "es" )
84
    }
85
    else if( pl.suffix( "([cs]h|ss|zz|x|s)", s ) ) {
86
      # 8. Change -ch, -sh, -ss, -zz, -x, -s to -es
87
      result <- pl.inflect( s, "", "es" )
88
    }
89
    else if( pl.suffix( "([aeo]lf|[^d]eaf|arf)", s ) ) {
90
      # 9. Change -f to -ves
91
      result <- pl.inflect( s, "f", "ves" )
92
    }
93
    else if( pl.suffix( "[nlw]ife", s ) ) {
94
      # 10. Change -fe to -ves
95
      result <- pl.inflect( s, "fe", "ves" )
96
    }
97
    else if( pl.suffix( "[aeiou]y", s ) ) {
98
      # 11. Change -[aeiou]y to -ys
99
      result <- pl.inflect( s, "", "s" )
100
    }
101
    else if( pl.suffix( "y", s ) ) {
102
      # 12. Change -y to -ies
103
      result <- pl.inflect( s, "y", "ies" )
104
    }
105
    else if( pl.suffix( "z", s ) ) {
106
      # x. Change -z to -zzes
107
      result <- pl.inflect( s, "", "zes" )
108
    }
109
    else {
110
      # 13. Default plural: add -s
111
      result <- pl.inflect( s, "", "s" )
112
    }
113
  }
114
115
  result
116
}
117
118
# -----------------------------------------------------------------------------
119
# Returns the given string (s) with its suffix replaced by r.
120
# -----------------------------------------------------------------------------
121
pl.inflect <- function( s, suffix, r ) {
122
  gsub( paste( suffix, "$", sep="" ), r, s )
123
}
124
125
# -----------------------------------------------------------------------------
126
# Answers whether the given string (s) has the given ending.
127
# -----------------------------------------------------------------------------
128
pl.suffix <- function( ending, s ) {
129
  grepl( paste( ending, "$", sep="" ), s )
130
}
131
132
# -----------------------------------------------------------------------------
133
# Answers whether the given string (s) is a noninflective noun.
134
# -----------------------------------------------------------------------------
135
pl.noninflective <- function( s ) {
136
  v <- c(
137
    "aircraft",
138
    "Bhutanese",
139
    "bison",
140
    "bream",
141
    "Burmese",
142
    "carp",
143
    "chassis",
144
    "Chinese",
145
    "clippers",
146
    "cod",
147
    "contretemps",
148
    "corps",
149
    "debris",
150
    "djinn",
151
    "eland",
152
    "elk",
153
    "flounder",
154
    "fracas",
155
    "gallows",
156
    "graffiti",
157
    "headquarters",
158
    "high-jinks",
159
    "homework",
160
    "hovercraft",
161
    "innings",
162
    "Japanese",
163
    "Lebanese",
164
    "mackerel",
165
    "means",
166
    "mews",
167
    "mice",
168
    "mumps",
169
    "news",
170
    "pincers",
171
    "pliers",
172
    "Portuguese",
173
    "proceedings",
174
    "salmon",
175
    "scissors",
176
    "sea-bass",
177
    "Senegalese",
178
    "shears",
179
    "Siamese",
180
    "Sinhalese",
181
    "spacecraft",
182
    "swine",
183
    "trout",
184
    "tuna",
185
    "Vietnamese",
186
    "watercraft",
187
    "whiting",
188
    "wildebeest"
189
  )
190
191
  is.element( s, v )
192
}
193
194
# -----------------------------------------------------------------------------
195
# Answers whether the given string (s) is an irregular plural.
196
# -----------------------------------------------------------------------------
197
pl.is.irregular.pl <- function( s ) {
198
  # Could be refactored with pl.irregular.pl...
199
  v <- c(
200
    "beef", "brother", "child", "cow", "ephemeris", "genie", "money",
201
    "mongoose", "mythos", "octopus", "ox", "soliloquy", "trilby"
202
  )
203
204
  is.element( s, v )
205
}
206
207
# -----------------------------------------------------------------------------
208
# Call to pluralize an irregular noun. Only call after confirming
209
# the noun is irregular via pl.is.irregular.pl.
210
# -----------------------------------------------------------------------------
211
pl.irregular.pl <- function( s ) {
212
  v <- list(
213
    "beef" = "beefs",
214
    "brother" = "brothers",
215
    "child" = "children",
216
    "cow" = "cows",
217
    "ephemeris" = "ephemerides",
218
    "genie" = "genies",
219
    "money" = "moneys",
220
    "mongoose" = "mongooses",
221
    "mythos" = "mythoi",
222
    "octopus" = "octopuses",
223
    "ox" = "oxen",
224
    "soliloquy" = "soliloquies",
225
    "trilby" = "trilbys"
226
  )
227
228
  # Faster version of v[[ s ]]
229
  .subset2( v, s )
230
}
231
232
# -----------------------------------------------------------------------------
233
# Answers whether the given string (s) pluralizes with -es.
234
# -----------------------------------------------------------------------------
235
pl.is.irregular.es <- function( s ) {
236
  v <- c(
237
    "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis",
238
    "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais",
239
    "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris",
240
    "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis",
241
    "polis", "rhinoceros", "sassafrass", "trellis"
242
  )
243
244
  is.element( s, v )
245
}
246
1247
A R/possessive.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# Returns leftmost n characters of s.
26
# -----------------------------------------------------------------------------
27
lstr <- function( s, n = 1 ) {
28
  substr( s, 0, n )
29
}
30
31
# -----------------------------------------------------------------------------
32
# Returns rightmost n characters of s.
33
# -----------------------------------------------------------------------------
34
rstr <- function( s, n = 1 ) {
35
  l <- nchar( s )
36
  substr( s, l - n + 1, l )
37
}
38
39
# -----------------------------------------------------------------------------
40
# Returns the possessive form of the given word, s.
41
# -----------------------------------------------------------------------------
42
pos <- function( s ) {
43
  lcs <- tolower( s )
44
  pronouns <- c( 'your', 'our', 'her', 'it', 'their' )
45
46
  if( lcs == 'my' ) {
47
    # Change "[Mm]y" to "[Mm]ine".
48
    s <- paste0( lstr( s, 1 ), "ine" )
49
  }
50
  else if( lcs %in% pronouns ) {
51
    # Append an s to most pronouns.
52
    s <- paste0( s, 's' )
53
  }
54
  else if( lcs != 'his' ) {
55
    # Possessive for all other words except 'his'.
56
    s <- paste0( s, ifelse( rstr( s, 1 ) == 's', "'" ,"'s" ) )
57
  }
58
59
  s
60
}
61
162
A README.md
1
![Logo](images/logo64.png)
2
3
# $application.title$
4
5
A text editor that uses [interpolated strings](https://en.wikipedia.org/wiki/String_interpolation) to reference externally defined values.
6
7
## Download
8
9
Download one of the following editions:
10
11
* [Windows](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.exe)
12
* [Linux](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.bin)
13
* [Java Archive](https://gitreleases.dev/gh/DaveJarvis/scrivenvar/latest/scrivenvar.jar)
14
15
## Run
16
17
Note that the first time the application runs, it will unpack itself into a local directory. Subsequent starts will be faster.
18
19
### Windows
20
21
On Windows, double-click the application to start. You will have to give the application permission to run.
22
23
### Linux
24
25
On Linux, run `chmod +x scrivenvar.bin` then `./scrivenvar.bin`.
26
27
### Other
28
29
On other platforms, download and install a full version of [OpenJDK 14](https://bell-sw.com/) that includes JavaFX module support, then run:
30
31
``` bash
32
java -jar scrivenvar.jar
33
```
34
35
## Features
36
37
* User-defined interpolated strings
38
* Real-time preview with variable substitution
39
* Auto-complete variable names based on variable values
40
* XML document transformation using XSLT3 or older
41
* Platform independent (Windows, Linux, MacOS)
42
* Spellcheck while typing
43
* R integration
44
45
## Usage
46
47
See the [detailed documentation](docs/README.md) for information about
48
using the application.
49
50
## Future Features
51
52
* Search and replace using variables
53
* Reorganize variable names
54
55
## Screenshot
56
57
![Screenshot](images/screenshot.png)
58
59
## License
60
61
This software is licensed under the [BSD 2-Clause License](LICENSE.md).
162
A _config.yaml
1
---
2
application:
3
  title: "Scrivenvar"
14
A build-template
1
#!/usr/bin/env bash
2
3
# -----------------------------------------------------------------------------
4
# Copyright 2020 Dave Jarvis
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining a
7
# copy of this software and associated documentation files (the
8
# "Software"), to deal in the Software without restriction, including
9
# without limitation the rights to use, copy, modify, merge, publish,
10
# distribute, sublicense, and/or sell copies of the Software, and to
11
# permit persons to whom the Software is furnished to do so, subject to
12
# the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be included
15
# in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
# -----------------------------------------------------------------------------
25
26
set -o errexit
27
set -o nounset
28
29
readonly SCRIPT_SRC="$(dirname "${BASH_SOURCE[${#BASH_SOURCE[@]} - 1]}")"
30
readonly SCRIPT_DIR="$(cd "${SCRIPT_SRC}" >/dev/null 2>&1 && pwd)"
31
readonly SCRIPT_NAME=$(basename "$0")
32
33
# -----------------------------------------------------------------------------
34
# The main entry point is responsible for parsing command-line arguments,
35
# changing to the appropriate directory, and running all commands requested
36
# by the user.
37
#
38
# $@ - Command-line arguments
39
# -----------------------------------------------------------------------------
40
main() {
41
  arguments "$@"
42
43
  $usage       && terminate 3
44
  requirements && terminate 4
45
  traps        && terminate 5
46
47
  directory    && terminate 6
48
  preprocess   && terminate 7
49
  execute      && terminate 8
50
  postprocess  && terminate 9
51
52
  terminate 0
53
}
54
55
# -----------------------------------------------------------------------------
56
# Perform all commands that the script requires.
57
#
58
# @return 0 - Indicate to terminate the script with non-zero exit level
59
# @return 1 - All tasks completed successfully (default)
60
# -----------------------------------------------------------------------------
61
execute() {
62
  return 1
63
}
64
65
# -----------------------------------------------------------------------------
66
# Changes to the script's working directory, provided it exists.
67
#
68
# @return 0 - Change directory failed
69
# @return 1 - Change directory succeeded
70
# -----------------------------------------------------------------------------
71
directory() {
72
  $log "Change directory"
73
  local result=1
74
75
  # Track whether change directory failed.
76
  cd "${SCRIPT_DIR}" > /dev/null 2>&1 || result=0
77
78
  return "${result}"
79
}
80
81
# -----------------------------------------------------------------------------
82
# Perform any initialization required prior to executing tasks.
83
#
84
# @return 0 - Preprocessing failed
85
# @return 1 - Preprocessing succeeded
86
# -----------------------------------------------------------------------------
87
preprocess() {
88
  $log "Preprocess"
89
90
  return 1
91
}
92
93
# -----------------------------------------------------------------------------
94
# Perform any clean up required prior to executing tasks.
95
#
96
# @return 0 - Postprocessing failed
97
# @return 1 - Postprocessing succeeded
98
# -----------------------------------------------------------------------------
99
postprocess() {
100
  $log "Postprocess"
101
102
  return 1
103
}
104
105
# -----------------------------------------------------------------------------
106
# Check that all required commands are available.
107
#
108
# @return 0 - At least one command is missing
109
# @return 1 - All commands are available
110
# -----------------------------------------------------------------------------
111
requirements() {
112
  $log "Verify requirements"
113
  local -r expected_count=${#DEPENDENCIES[@]}
114
  local total_count=0
115
116
  # Verify that each command exists.
117
  for dependency in "${DEPENDENCIES[@]}"; do
118
    # Extract the command name [0] and URL [1].
119
    IFS=',' read -ra dependent <<< "${dependency}"
120
121
    required "${dependent[0]}" "${dependent[1]}"
122
    total_count=$(( total_count + $? ))
123
  done
124
125
  unset IFS
126
127
  # Total dependencies found must match the expected number.
128
  # Integer-only division rounds down.
129
  return $(( total_count / expected_count ))
130
}
131
132
# -----------------------------------------------------------------------------
133
# Called before terminating the script.
134
# -----------------------------------------------------------------------------
135
cleanup() {
136
  $log "Cleanup"
137
}
138
139
# -----------------------------------------------------------------------------
140
# Terminates the program immediately.
141
# -----------------------------------------------------------------------------
142
trap_control_c() {
143
  $log "Interrupted"
144
  cleanup
145
  error "⯃"
146
  terminate 1
147
}
148
149
# -----------------------------------------------------------------------------
150
# Configure signal traps.
151
#
152
# @return 1 - Signal traps are set.
153
# -----------------------------------------------------------------------------
154
traps() {
155
  # Suppress echoing ^C if pressed.
156
  stty -echoctl
157
  trap trap_control_c INT
158
159
  return 1
160
}
161
162
# -----------------------------------------------------------------------------
163
# Check for a required command.
164
#
165
# $1 - Command or file to check for existence
166
# $2 - Command's website (e.g., download for binaries and source code)
167
#
168
# @return 0 - Command is missing
169
# @return 1 - Command exists
170
# -----------------------------------------------------------------------------
171
required() {
172
  local result=0
173
174
  test -f "$1" || \
175
  command -v "$1" > /dev/null 2>&1 && result=1 || \
176
    warning "Missing: $1 ($2)"
177
178
  return ${result}
179
}
180
181
# -----------------------------------------------------------------------------
182
# Show acceptable command-line arguments.
183
#
184
# @return 0 - Indicate script may not continue
185
# -----------------------------------------------------------------------------
186
utile_usage() {
187
  printf "Usage: %s [OPTIONS...]\n\n" "${SCRIPT_NAME}" >&2
188
189
  # Number of spaces to pad after the longest long argument.
190
  local -r PADDING=2
191
192
  # Determine the longest long argument to adjust spacing.
193
  local -r LEN=$(printf '%s\n' "${ARGUMENTS[@]}" | \
194
    awk -F"," '{print length($2)+'${PADDING}'}' | sort -n | tail -1)
195
196
  local duplicates
197
198
  for argument in "${ARGUMENTS[@]}"; do
199
    # Extract the short [0] and long [1] arguments and description [2].
200
    arg=("$(echo ${argument} | cut -d ',' -f1)" \
201
         "$(echo ${argument} | cut -d ',' -f2)" \
202
         "$(echo ${argument} | cut -d ',' -f3-)")
203
204
    duplicates+=("${arg[0]}")
205
206
    printf "  -%s, --%-${LEN}s%s\n" "${arg[0]}" "${arg[1]}" "${arg[2]}" >&2
207
  done
208
209
  # Sort the arguments to make sure no duplicates exist.
210
  duplicates=$(echo "${duplicates[@]}" | tr ' ' '\n' | sort | uniq -c -d)
211
212
  # Warn the developer that there's a duplicate command-line option.
213
  if [ -n "${duplicates}" ]; then
214
    # Trim all the whitespaces
215
    duplicates=$(echo "${duplicates}" | xargs echo -n)
216
    error "Duplicate command-line argument exists: ${duplicates}"
217
  fi
218
219
  return 0
220
}
221
222
# -----------------------------------------------------------------------------
223
# Write coloured text to standard output.
224
#
225
# $1 - Text to write
226
# $2 - Text's colour
227
# -----------------------------------------------------------------------------
228
coloured_text() {
229
  printf "%b%s%b\n" "$2" "$1" "${COLOUR_OFF}"
230
}
231
232
# -----------------------------------------------------------------------------
233
# Write a warning message to standard output.
234
#
235
# $1 - Text to write
236
# -----------------------------------------------------------------------------
237
warning() {
238
  coloured_text "$1" "${COLOUR_WARNING}"
239
}
240
241
# -----------------------------------------------------------------------------
242
# Write an error message to standard output.
243
#
244
# $1 - Text to write
245
# -----------------------------------------------------------------------------
246
error() {
247
  coloured_text "$1" "${COLOUR_ERROR}"
248
}
249
250
# -----------------------------------------------------------------------------
251
# Write a timestamp and message to standard output.
252
#
253
# $1 - Text to write
254
# -----------------------------------------------------------------------------
255
utile_log() {
256
  printf "[%s] " "$(date +%H:%M:%S.%4N)"
257
  coloured_text "$1" "${COLOUR_LOGGING}"
258
}
259
260
# -----------------------------------------------------------------------------
261
# Perform no operations.
262
#
263
# return 1 - Success
264
# -----------------------------------------------------------------------------
265
noop() {
266
  return 1
267
}
268
269
# -----------------------------------------------------------------------------
270
# Exit the program with a given exit code.
271
#
272
# $1 - Exit code
273
# -----------------------------------------------------------------------------
274
terminate() {
275
  exit "$1"
276
}
277
278
# -----------------------------------------------------------------------------
279
# Set global variables from command-line arguments.
280
# -----------------------------------------------------------------------------
281
arguments() {
282
  while [ "$#" -gt "0" ]; do
283
    local consume=1
284
285
    case "$1" in
286
      -V|--verbose)
287
        log=utile_log
288
      ;;
289
      -h|-\?|--help)
290
        usage=utile_usage
291
      ;;
292
      *)
293
        set +e
294
        argument "$@"
295
        consume=$?
296
        set -e
297
      ;;
298
    esac
299
300
    shift ${consume}
301
  done
302
}
303
304
# -----------------------------------------------------------------------------
305
# Parses a single command-line argument. This must return a value greater
306
# than or equal to 1, otherwise parsing the command-line arguments will
307
# loop indefinitely.
308
#
309
# @return The number of arguments to consume (1 by default).
310
# -----------------------------------------------------------------------------
311
argument() {
312
  return 1
313
}
314
315
# ANSI colour escape sequences.
316
readonly COLOUR_BLUE='\033[1;34m'
317
readonly COLOUR_PINK='\033[1;35m'
318
readonly COLOUR_DKGRAY='\033[30m'
319
readonly COLOUR_DKRED='\033[31m'
320
readonly COLOUR_LTRED='\033[1;31m'
321
readonly COLOUR_YELLOW='\033[1;33m'
322
readonly COLOUR_OFF='\033[0m'
323
324
# Colour definitions used by script.
325
COLOUR_LOGGING=${COLOUR_BLUE}
326
COLOUR_WARNING=${COLOUR_YELLOW}
327
COLOUR_ERROR=${COLOUR_LTRED}
328
329
# Define required commands to check when script starts.
330
DEPENDENCIES=(
331
  "awk,https://www.gnu.org/software/gawk/manual/gawk.html"
332
  "cut,https://www.gnu.org/software/coreutils"
333
)
334
335
# Define help for command-line arguments.
336
ARGUMENTS=(
337
  "V,verbose,Log messages while processing"
338
  "h,help,Show this help message then exit"
339
)
340
341
# These functions may be set to utile delegates while parsing arguments.
342
usage=noop
343
log=noop
344
1345
A build.gradle
1
plugins {
2
  id 'application'
3
  id 'org.openjfx.javafxplugin' version '0.0.8'
4
  id 'com.palantir.git-version' version '0.12.3'
5
  id 'org.beryx.jlink' version '2.16.2'
6
}
7
8
repositories {
9
  mavenCentral()
10
  jcenter()
11
12
  maven {
13
    url 'https://oss.sonatype.org/content/repositories/snapshots/'
14
  }
15
16
  maven {
17
    url "https://nexus.bedatadriven.com/content/groups/public"
18
  }
19
}
20
21
// Assume an überjar unless targetOs is set.
22
String[] os = ["win", "mac", "linux"]
23
24
if (project.hasProperty('targetOs')) {
25
  if ("windows" == targetOs) {
26
    os = ["win"]
27
  } else {
28
    os = [targetOs]
29
  }
30
}
31
32
dependencies {
33
  // JavaFX
34
  implementation 'org.reactfx:reactfx:1.4.1'
35
  implementation 'org.controlsfx:controlsfx:11.0.1'
36
  implementation 'org.fxmisc.richtext:richtextfx:0.10.5'
37
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
38
  implementation 'com.miglayout:miglayout-javafx:5.2'
39
  implementation('com.dlsc.preferencesfx:preferencesfx-core:11.6.0') {
40
    exclude group: 'org.openjfx'
41
  }
42
  implementation('de.jensd:fontawesomefx-commons:11.0') {
43
    exclude group: 'org.openjfx'
44
  }
45
  implementation('de.jensd:fontawesomefx-fontawesome:4.7.0-11') {
46
    exclude group: 'org.openjfx'
47
  }
48
49
  // Markdown
50
  implementation 'com.vladsch.flexmark:flexmark:0.62.2'
51
  implementation 'com.vladsch.flexmark:flexmark-ext-definition:0.62.2'
52
  implementation 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.62.2'
53
  implementation 'com.vladsch.flexmark:flexmark-ext-superscript:0.62.2'
54
  implementation 'com.vladsch.flexmark:flexmark-ext-tables:0.62.2'
55
  implementation 'com.vladsch.flexmark:flexmark-ext-typographic:0.62.2'
56
57
  // YAML
58
  implementation 'com.fasterxml.jackson.core:jackson-core:2.11.0'
59
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0'
60
  implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.0'
61
  implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.0'
62
  implementation 'org.yaml:snakeyaml:1.26'
63
64
  // XML and XSL
65
  implementation 'com.ximpleware:vtd-xml:2.13.4'
66
  implementation 'net.sf.saxon:Saxon-HE:10.1'
67
  implementation 'xalan:xalan:2.7.2'
68
69
  // HTML parsing and rendering
70
  implementation 'org.jsoup:jsoup:1.13.1'
71
  implementation 'org.xhtmlrenderer:flying-saucer-core:9.1.20'
72
73
  // R
74
  implementation 'org.renjin:renjin-script-engine:3.5-beta76'
75
76
  // SVG
77
  implementation 'org.apache.xmlgraphics:batik-anim:1.13'
78
  implementation 'org.apache.xmlgraphics:batik-awt-util:1.13'
79
  implementation 'org.apache.xmlgraphics:batik-bridge:1.13'
80
  implementation 'org.apache.xmlgraphics:batik-css:1.13'
81
  implementation 'org.apache.xmlgraphics:batik-dom:1.13'
82
  implementation 'org.apache.xmlgraphics:batik-ext:1.13'
83
  implementation 'org.apache.xmlgraphics:batik-gvt:1.13'
84
  implementation 'org.apache.xmlgraphics:batik-parser:1.13'
85
  implementation 'org.apache.xmlgraphics:batik-script:1.13'
86
  implementation 'org.apache.xmlgraphics:batik-svg-dom:1.13'
87
  implementation 'org.apache.xmlgraphics:batik-svggen:1.13'
88
  implementation 'org.apache.xmlgraphics:batik-transcoder:1.13'
89
  implementation 'org.apache.xmlgraphics:batik-util:1.13'
90
  implementation 'org.apache.xmlgraphics:batik-xml:1.13'
91
92
  // Spelling
93
  implementation fileTree(include: ['**/*.jar'], dir: 'libs')
94
95
  // Misc.
96
  implementation 'org.ahocorasick:ahocorasick:0.4.0'
97
  implementation 'org.apache.commons:commons-configuration2:2.7'
98
  implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
99
  implementation 'javax.validation:validation-api:2.0.1.Final'
100
101
  def fx = ['controls', 'graphics', 'fxml', 'swing']
102
103
  fx.each { fxitem ->
104
    os.each { ositem ->
105
      runtimeOnly "org.openjfx:javafx-${fxitem}:${javafx.version}:${ositem}"
106
    }
107
  }
108
109
  testImplementation('org.junit.jupiter:junit-jupiter-api:5.4.2')
110
  testRuntime('org.junit.jupiter:junit-jupiter-engine:5.4.2')
111
}
112
113
javafx {
114
  version = "14"
115
  modules = ['javafx.controls', 'javafx.swing']
116
}
117
118
compileJava {
119
  options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
120
}
121
122
application {
123
  applicationName = 'scrivenvar'
124
  mainClassName = "com.${applicationName}.Main"
125
126
  applicationDefaultJvmArgs = [
127
      "--add-opens=javafx.controls/javafx.scene.control=ALL-UNNAMED",
128
      "--add-opens=javafx.controls/javafx.scene.control.skin=ALL-UNNAMED",
129
      "--add-opens=javafx.graphics/com.sun.javafx.css=ALL-UNNAMED",
130
  ]
131
}
132
133
version = gitVersion()
134
sourceCompatibility = JavaVersion.VERSION_11
135
136
def launcherClassName = "com.${applicationName}.Launcher"
137
138
def propertiesFile = new File("src/main/resources/com/${applicationName}/app.properties")
139
propertiesFile.write("application.version=${version}")
140
141
jar {
142
  duplicatesStrategy = DuplicatesStrategy.EXCLUDE
143
144
  manifest {
145
    attributes 'Main-Class': launcherClassName
146
  }
147
148
  from {
149
    (configurations.runtimeClasspath.findAll { !it.path.endsWith(".pom") }).collect {
150
      it.isDirectory() ? it : zipTree(it)
151
    }
152
  }
153
154
  archiveFileName = "${applicationName}.jar"
155
156
  exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
157
}
158
159
distributions {
160
  main {
161
    distributionBaseName = applicationName
162
    contents {
163
      from { ['LICENSE.md', 'README.md'] }
164
      into('images') {
165
        from { 'images' }
166
      }
167
    }
168
  }
169
}
170
171
test {
172
  useJUnitPlatform()
173
}
174
1175
A docs/README.md
1
## Documents
2
3
See the following documents for more information:
4
5
* [definitions.md](definitions.md) -- Definitions and interpolation
6
* [r.md](r.md) -- Call R functions within R Markdown documents
7
* [svg.md](svg.md) -- Fix known issues with displaying SVG files
8
* [credits.md](credits.md) -- Thanks to authors of contributing projects
9
110
A docs/credits.md
1
# Credits
2
3
* Karl Tauber: [Markdown Writer FX](https://github.com/JFormDesigner/markdown-writer-fx)
4
* 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)
5
* Mikael Grev: [MigLayout](http://www.miglayout.com/)
6
* Tom Eugelink: [MigPane](https://github.com/mikaelgrev/miglayout/blob/master/javafx/src/main/java/org/tbee/javafx/scene/layout/fxml/MigPane.java)
7
* Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
8
* Dieter Holz, [PreferencesFX](https://github.com/dlsc-software-consulting-gmbh/PreferencesFX)
9
* David Croft, [File Preferences](http://www.davidc.net/programming/java/java-preferences-using-file-backing-store)
10
* Alex Bertram, [Renjin](https://www.renjin.org/)
11
* Vladimir Schneider: [flexmark](https://github.com/vsch/flexmark-java)
12
* Michael Kay, [XSLT Processor](http://www.saxonica.com/)
13
* Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet)
14
115
A docs/definitions.md
1
# Introduction
2
3
This document describes how to use the application.
4
5
# Variable definitions
6
7
Variable definitions provide a way to insert key names having associated values into a document. The variable names and values are declared inside an external file using the [YAML](http://www.yaml.org/) file format. Simply put, variables are written in the file as follows:
8
9
```
10
key: value
11
```
12
13
Any number of variables can be defined, in any order:
14
15
```
16
key_1: Value 1
17
key_2: Value 2
18
```
19
20
Variables can reference other variables by enclosing the key name within dollar symbols:
21
22
```
23
key: Value
24
key_1: $key$ 1
25
key_2: $key$ 2
26
```
27
28
Variables can use a nested structure to help group related information:
29
30
```
31
novel:
32
  title: Book Title
33
  author: Author Name
34
  isbn: 978-3-16-148410-0
35
```
36
37
Use a period to reference nested keys, such as:
38
39
```
40
novel:
41
  author: Author Name
42
copyright:
43
  owner: $novel.author$
44
```
45
46
Save the variable definitions in a file having an extension of `.yaml` or `.yml`.
47
48
# Document editing
49
50
The application's purpose is to completely separate the document's content from its presentation. To achieve this, documents are composed using a [plain text](http://spec.commonmark.org/0.28/) format.
51
52
## Create document
53
54
Start a new document as follows:
55
56
1. Start the application.
57
1. Click **File → New** to create an empty document to edit.
58
1. Click **File → Open** to open a variable definition file.
59
1. Change **Source Files** to **Definition Files** to list definition files.
60
1. Browse to and select a file saved with a `.yaml` or `.yml` extension.
61
1. Click **Open**.
62
63
The variable definitions appear in the variable definition pane under the heading of **Definitions**.
64
65
## Edit document
66
67
Edit the document as normal. Notice how the preview pane updates as new content is added. The toolbar shows various icons that perform different formatting operations. Try them to see how they appear in the preview pane. Other operations not shown on the toolbar include:
68
69
* Struck text (enclose the words within `~~` and `~~`)
70
* Horizontal rule (use `---` on an otherwise empty line).
71
72
The preview pane shows one way to interpret and format the document, but many other presentations are possible.
73
74
## Insert variable
75
76
Let's assume that the variable definitions loaded into the application include:
77
78
```
79
novel:
80
  title: Diary of $novel.author$
81
  author: Anne Frank
82
```
83
84
To reference a variable, type in the key name enclosed within dollar symbols, such as:
85
86
```
87
The novel "$novel.title$" is one of the most widely read books in the world.
88
```
89
90
The preview pane shows:
91
92
> The novel "Diary of Anne Frank" is one of the most widely read books in the world.
93
94
As it is laborious to type in variable names, it is possible to inject the variable name using autocomplete. Accomplish this as follows:
95
96
1. Create a new file.
97
1. Type in a partial variable value, such as **Dia**.
98
1. Press `Ctrl+Space` (hold down the `Control` key and tap the spacebar).
99
100
The editor shows:
101
102
```
103
$novel.title$
104
```
105
106
The preview pane shows:
107
108
```
109
Diary of Anne Frank
110
```
111
112
The variable name is inserted into the document and the preview pane shows the variable's value.
113
1114
A docs/r.md
1
# Introduction
2
3
This document describes how to use the [R](https://www.r-project.org/)
4
programming language from within the application. The application uses an
5
interpreter known as [Renjin](https://www.renjin.org/) to integrate with R.
6
7
# Hello world
8
9
Complete the following steps to see R in action:
10
11
1. Start the application.
12
1. Click **File → New** to create a new file.
13
1. Click **File → Save As**.
14
1. Set **Name** to: `addition.Rmd`
15
1. Click **Save**.
16
17
Setting the file name extension tells the application what processor to
18
use when transforming the contents for display in the preview pane. Continue
19
by typing in the following text, including the backticks:
20
21
```r
22
`r#1 + 1`
23
```
24
25
The preview pane shows the result of `1` plus `1`:
26
27
```
28
2.0
29
```
30
31
# Bootstrap script
32
33
Being able to run R code while editing an R Markdown document is convenient.
34
Having the ability to call functions is where the power of R can be
35
leveraged.
36
37
Complete the following steps to call an R function from your own library:
38
39
1. Click **File → New** to create a new file.
40
1. Click **File → Save As**.
41
1. Browse to your home directory.
42
1. Set **Name** to: `library.R`.
43
1. Click **Save**.
44
1. Set the contents to:
45
    ``` r
46
    sum <- function( a, b ) {
47
      a + b
48
    }
49
    ```
50
1. Click the **Save** icon.
51
1. Click **R → Script**.
52
1. Set the **R Startup Script** contents to:
53
    ``` r
54
    source( 'library.R' );
55
    ```
56
1. Click **OK**.
57
1. Create a new file.
58
1. Set the contents to:
59
    ``` r
60
    `r#sum( 5, 5 )`
61
    ```
62
1. Save the file as `sum.R`.
63
64
The preview panel shows the result of calling the `sum` function:
65
66
```
67
10.0
68
```
69
70
This shows how the bootstrap script can load `library.R`, which defines
71
a `sum` function that is called by name in the Markdown document.
72
73
# Working directory
74
75
R files may be sourced from any directory, not just the user's home
76
directory. Accomplish this as follows:
77
78
1. Click **R → Directory**.
79
1. Set **Directory** to a different directory.
80
1. Click **OK**.
81
1. Create the directory if it does not exist.
82
1. Move `library.R` into the directory.
83
1. Append a new function to `library.R` as follows:
84
    ``` r
85
    mul <- function( a, b ) {
86
      a * b
87
    }
88
    ```
89
1. Click **R → Script**.
90
1. Set the **R Startup Script** contents to:
91
    ``` r
92
    setwd( '$application.r.working.directory$' );
93
    source( 'library.R' );
94
    ```
95
1. Change `sum.Rmd` to:
96
    ``` r
97
    `r#mul( 5, 5 )`
98
    ```
99
1. Close the file `sum.Rmd`.
100
1. Confirm saving the file when prompted.
101
1. Re-open `sum.Rmd`.
102
103
The preview panel shows:
104
105
```
106
25.0
107
```
108
109
Calling `setwd` using `'$application.r.working.directory$'` changes the
110
working directory where the R engine searches for source files.
111
112
# YAML definitions
113
114
To see how variable definitions work in R, try the following:
115
116
1. Create a new file.
117
1. Change the contents to (use spaces not tabs):
118
    ``` yaml
119
    project:
120
      title: Project Title
121
      author: Author Name
122
    ```
123
1. Save the file as `definitions.yaml`.
124
1. Click **File → Open**.
125
1. Set **Source Files** to **Definition Files**.
126
1. Select `definitions.yaml`.
127
1. Click **Open**.
128
1. Open `sum.Rmd` if it is not already open.
129
1. Type: `je`
130
1. Press `Ctrl+Space`
131
132
The editor inserts the following text (matches `je` against Pro**je**ct):
133
134
``` r
135
`r#x( v$project$title )`
136
```
137
138
The preview panel shows:
139
140
```
141
r#x( 'Project Title' )
142
```
143
144
This is because the application inserts definition reference names based
145
on the type of file being edited. By default, the R engine does not have
146
a function named `x` defined.
147
148
Continue as follows:
149
150
1. Click **R → Script**.
151
1. Append the following:
152
    ``` r
153
    x <- function( s ) {
154
      tryCatch( {
155
        r = eval( parse( text = s ) )
156
157
        ifelse( is.atomic( r ), r, s );
158
      },
159
      warning = function( w ) { s },
160
      error = function( e ) { s } )
161
    }
162
    ```
163
1. Click **OK**.
164
1. Close and re-open `sum.Rmd`.
165
166
The preview panel shows:
167
168
```
169
25.0
170
171
Project Title
172
```
173
174
The `x` function attempts to evaluate the expression defined by the YAML
175
variable. This means that the YAML definitions can also include expressions
176
that R is capable of evaluating.
177
178
While the `x` function can be defined within the R Startup Script, it is
179
better practice to put it into its own library so that it can be reused
180
outside of the application.
181
1182
A docs/svg.md
1
# Introduction
2
3
The Scalable Vector Graphics (SVG) drawing software---[Batik](https://xmlgraphics.apache.org/batik/)---that's used by the application may be unable to read certain SVG files produced by [Inkscape](https://inkscape.org/). The result is that embedding the vector graphics files may trigger the following issues:
4
5
* Unable to create nested element
6
* Black blocks, no text displayed
7
* Black text instead of coloured
8
9
The remainder of this document explains these problems and how to fix them.
10
11
# Nested element
12
13
When referencing a vector graphic using Markdown, the status bar may show the following error:
14
15
> The current document is unable to create an element of the requested type (namespace: http://www.w3.org/2000/svg, name: flowRoot).
16
17
This error is due to a version mismatch of the `flowRoot` element that Inkscape creates.
18
19
## Fix
20
21
Resolve the issue by changing the SVG version number as follows:
22
23
1. Edit the vector graphics file using any text editor.
24
1. Find `version="1.1"` and change it to `version="1.2"`.
25
1. Save the file.
26
27
The SVG will now appear inside the application; however, the text may appear as black blocks.
28
29
# Black blocks
30
31
Depending on how text is added to a vector graphic in Inkscape, the text may be inserted within an element called a `flowRoot`. Although Batik recognizes `flowRoot` for SVG version 1.2, it cannot fully interpret the contents. Black blocks are drawn instead of the text, such as those depicted in the following figure:
32
33
![Missing text](images/blocked-text.png)
34
35
## Fix
36
37
Resolve the issue by "unflowing" all text elements as follows:
38
39
1. Start Inkscape.
40
1. Load the SVG file.
41
1. Select all the text elements.
42
1. Click **Text → Unflow**.
43
44
The text may change size and position; recreate the text without dragging using the text tool. After all the text areas have been recreated, continue as follows:
45
46
1. Click **Edit → XML Editor**.
47
1. Expand the **XML Editor** to see more elements.
48
1. Delete all elements named `svg:flowRoot`.
49
1. Save the file.
50
51
When the illustration is reloaded, the black blocks will have disappeared, but the text elements ignore any assigned colour.
52
53
# Black text
54
55
When an SVG `style` attribute contains a reference to `-inkscape-font-specification`, Batik ignores all values that follow said reference. This results in black text, such as:
56
57
![Black text](images/black-text.png)
58
59
## Fix
60
61
Resolve the issue of colourless text as follows:
62
63
1. Open the SVG file in a plain text editor.
64
1. Remove all references `-inkscape-font-specification:'<FONT>';`, including the trailing (or leading) semicolon.
65
1. Save the file.
66
67
When the illustration is reloaded, the colours will have reappeared, such as:
68
69
![Resolved text](images/resolved-text.png)
70
171
A gradle.properties
1
org.gradle.jvmargs=-Xmx1G -XX:MaxPermSize=512m
2
13
A images/architecture/architecture.png
Binary file
A images/architecture/architecture.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<svg
3
   xmlns:dc="http://purl.org/dc/elements/1.1/"
4
   xmlns:cc="http://creativecommons.org/ns#"
5
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
6
   xmlns:svg="http://www.w3.org/2000/svg"
7
   xmlns="http://www.w3.org/2000/svg"
8
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
9
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
10
   inkscape:export-ydpi="150.0097"
11
   inkscape:export-xdpi="150.0097"
12
   sodipodi:docname="architecture.svg"
13
   viewBox="0 0 764.4414 811.46748"
14
   height="811.46747"
15
   width="764.44141"
16
   id="svg4610"
17
   version="1.2"
18
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
19
  <metadata
20
     id="metadata4616">
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 />
28
      </cc:Work>
29
    </rdf:RDF>
30
  </metadata>
31
  <defs
32
     id="defs4614">
33
    <marker
34
       inkscape:stockid="Arrow1Mend"
35
       orient="auto"
36
       refY="0"
37
       refX="0"
38
       id="marker10933"
39
       style="overflow:visible"
40
       inkscape:isstock="true">
41
      <path
42
         id="path10931"
43
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
44
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
45
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
46
         inkscape:connector-curvature="0" />
47
    </marker>
48
    <marker
49
       inkscape:stockid="Arrow1Mend"
50
       orient="auto"
51
       refY="0"
52
       refX="0"
53
       id="marker9893"
54
       style="overflow:visible"
55
       inkscape:isstock="true">
56
      <path
57
         id="path9891"
58
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
59
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
60
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
61
         inkscape:connector-curvature="0" />
62
    </marker>
63
    <marker
64
       inkscape:collect="always"
65
       inkscape:isstock="true"
66
       style="overflow:visible"
67
       id="marker9767"
68
       refX="0"
69
       refY="0"
70
       orient="auto"
71
       inkscape:stockid="Arrow1Mend">
72
      <path
73
         inkscape:connector-curvature="0"
74
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
75
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
76
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
77
         id="path9765" />
78
    </marker>
79
    <marker
80
       inkscape:collect="always"
81
       inkscape:stockid="Arrow1Mend"
82
       orient="auto"
83
       refY="0"
84
       refX="0"
85
       id="marker9761"
86
       style="overflow:visible"
87
       inkscape:isstock="true">
88
      <path
89
         id="path9759"
90
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
91
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
92
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
93
         inkscape:connector-curvature="0" />
94
    </marker>
95
    <marker
96
       inkscape:isstock="true"
97
       style="overflow:visible"
98
       id="marker9750"
99
       refX="0"
100
       refY="0"
101
       orient="auto"
102
       inkscape:stockid="Arrow1Mend">
103
      <path
104
         inkscape:connector-curvature="0"
105
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
106
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
107
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
108
         id="path9748" />
109
    </marker>
110
    <marker
111
       inkscape:isstock="true"
112
       style="overflow:visible"
113
       id="marker9715"
114
       refX="0"
115
       refY="0"
116
       orient="auto"
117
       inkscape:stockid="Arrow1Mend">
118
      <path
119
         inkscape:connector-curvature="0"
120
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
121
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
122
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
123
         id="path9713" />
124
    </marker>
125
    <marker
126
       inkscape:collect="always"
127
       inkscape:stockid="Arrow1Mend"
128
       orient="auto"
129
       refY="0"
130
       refX="0"
131
       id="marker9685"
132
       style="overflow:visible"
133
       inkscape:isstock="true">
134
      <path
135
         id="path9683"
136
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
137
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
138
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
139
         inkscape:connector-curvature="0" />
140
    </marker>
141
    <marker
142
       inkscape:collect="always"
143
       inkscape:stockid="Arrow1Mend"
144
       orient="auto"
145
       refY="0"
146
       refX="0"
147
       id="marker9679"
148
       style="overflow:visible"
149
       inkscape:isstock="true">
150
      <path
151
         id="path9677"
152
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
153
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
154
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
155
         inkscape:connector-curvature="0" />
156
    </marker>
157
    <marker
158
       inkscape:collect="always"
159
       inkscape:isstock="true"
160
       style="overflow:visible"
161
       id="marker9640"
162
       refX="0"
163
       refY="0"
164
       orient="auto"
165
       inkscape:stockid="Arrow1Mend">
166
      <path
167
         inkscape:connector-curvature="0"
168
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
169
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
170
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
171
         id="path9638" />
172
    </marker>
173
    <marker
174
       inkscape:collect="always"
175
       inkscape:isstock="true"
176
       style="overflow:visible"
177
       id="marker9513"
178
       refX="0"
179
       refY="0"
180
       orient="auto"
181
       inkscape:stockid="Arrow1Mend">
182
      <path
183
         inkscape:connector-curvature="0"
184
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
185
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
186
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
187
         id="path9511" />
188
    </marker>
189
    <marker
190
       inkscape:stockid="Arrow1Mend"
191
       orient="auto"
192
       refY="0"
193
       refX="0"
194
       id="marker9509"
195
       style="overflow:visible"
196
       inkscape:isstock="true">
197
      <path
198
         id="path9507"
199
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
200
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
201
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
202
         inkscape:connector-curvature="0" />
203
    </marker>
204
    <marker
205
       inkscape:isstock="true"
206
       style="overflow:visible"
207
       id="marker9505"
208
       refX="0"
209
       refY="0"
210
       orient="auto"
211
       inkscape:stockid="Arrow1Mend">
212
      <path
213
         inkscape:connector-curvature="0"
214
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
215
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
216
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
217
         id="path9503" />
218
    </marker>
219
    <marker
220
       inkscape:collect="always"
221
       inkscape:stockid="Arrow1Mend"
222
       orient="auto"
223
       refY="0"
224
       refX="0"
225
       id="marker9479"
226
       style="overflow:visible"
227
       inkscape:isstock="true">
228
      <path
229
         id="path9477"
230
         d="M 0,0 5,-5 -12.5,0 5,5 Z"
231
         style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke:#05556e;stroke-width:1.00000003pt;stroke-opacity:1"
232
         transform="matrix(-0.4,0,0,-0.4,-4,0)"
233
         inkscape:connector-curvature="0" />
234
    </marker>
235
    <clipPath
236
       id="ID000001">
237
      <rect
238
         id="rect6"
239
         height="961.125"
240
         width="1381.6169"
241
         y="-43.688"
242
         x="-62.683998" />
243
    </clipPath>
244
    <filter
245
       id="filter2842"
246
       inkscape:label="Drop Shadow"
247
       style="color-interpolation-filters:sRGB;">
248
      <feFlood
249
         id="feFlood2832"
250
         result="flood"
251
         flood-color="rgb(0,0,0)"
252
         flood-opacity="0.498039" />
253
      <feComposite
254
         id="feComposite2834"
255
         result="composite1"
256
         operator="in"
257
         in2="SourceGraphic"
258
         in="flood" />
259
      <feGaussianBlur
260
         id="feGaussianBlur2836"
261
         result="blur"
262
         stdDeviation="2"
263
         in="composite1" />
264
      <feOffset
265
         id="feOffset2838"
266
         result="offset"
267
         dy="3"
268
         dx="3" />
269
      <feComposite
270
         id="feComposite2840"
271
         result="composite2"
272
         operator="over"
273
         in2="offset"
274
         in="SourceGraphic" />
275
    </filter>
276
    <filter
277
       id="filter2854"
278
       inkscape:label="Drop Shadow"
279
       style="color-interpolation-filters:sRGB;">
280
      <feFlood
281
         id="feFlood2844"
282
         result="flood"
283
         flood-color="rgb(0,0,0)"
284
         flood-opacity="0.498039" />
285
      <feComposite
286
         id="feComposite2846"
287
         result="composite1"
288
         operator="in"
289
         in2="SourceGraphic"
290
         in="flood" />
291
      <feGaussianBlur
292
         id="feGaussianBlur2848"
293
         result="blur"
294
         stdDeviation="2"
295
         in="composite1" />
296
      <feOffset
297
         id="feOffset2850"
298
         result="offset"
299
         dy="3"
300
         dx="3" />
301
      <feComposite
302
         id="feComposite2852"
303
         result="composite2"
304
         operator="over"
305
         in2="offset"
306
         in="SourceGraphic" />
307
    </filter>
308
    <filter
309
       id="filter2866"
310
       inkscape:label="Drop Shadow"
311
       style="color-interpolation-filters:sRGB;">
312
      <feFlood
313
         id="feFlood2856"
314
         result="flood"
315
         flood-color="rgb(0,0,0)"
316
         flood-opacity="0.498039" />
317
      <feComposite
318
         id="feComposite2858"
319
         result="composite1"
320
         operator="in"
321
         in2="SourceGraphic"
322
         in="flood" />
323
      <feGaussianBlur
324
         id="feGaussianBlur2860"
325
         result="blur"
326
         stdDeviation="2"
327
         in="composite1" />
328
      <feOffset
329
         id="feOffset2862"
330
         result="offset"
331
         dy="3"
332
         dx="3" />
333
      <feComposite
334
         id="feComposite2864"
335
         result="composite2"
336
         operator="over"
337
         in2="offset"
338
         in="SourceGraphic" />
339
    </filter>
340
    <filter
341
       id="filter2878"
342
       inkscape:label="Drop Shadow"
343
       style="color-interpolation-filters:sRGB;">
344
      <feFlood
345
         id="feFlood2868"
346
         result="flood"
347
         flood-color="rgb(0,0,0)"
348
         flood-opacity="0.498039" />
349
      <feComposite
350
         id="feComposite2870"
351
         result="composite1"
352
         operator="in"
353
         in2="SourceGraphic"
354
         in="flood" />
355
      <feGaussianBlur
356
         id="feGaussianBlur2872"
357
         result="blur"
358
         stdDeviation="2"
359
         in="composite1" />
360
      <feOffset
361
         id="feOffset2874"
362
         result="offset"
363
         dy="3"
364
         dx="3" />
365
      <feComposite
366
         id="feComposite2876"
367
         result="composite2"
368
         operator="over"
369
         in2="offset"
370
         in="SourceGraphic" />
371
    </filter>
372
    <filter
373
       id="filter2890"
374
       inkscape:label="Drop Shadow"
375
       style="color-interpolation-filters:sRGB;">
376
      <feFlood
377
         id="feFlood2880"
378
         result="flood"
379
         flood-color="rgb(0,0,0)"
380
         flood-opacity="0.498039" />
381
      <feComposite
382
         id="feComposite2882"
383
         result="composite1"
384
         operator="in"
385
         in2="SourceGraphic"
386
         in="flood" />
387
      <feGaussianBlur
388
         id="feGaussianBlur2884"
389
         result="blur"
390
         stdDeviation="2"
391
         in="composite1" />
392
      <feOffset
393
         id="feOffset2886"
394
         result="offset"
395
         dy="3"
396
         dx="3" />
397
      <feComposite
398
         id="feComposite2888"
399
         result="composite2"
400
         operator="over"
401
         in2="offset"
402
         in="SourceGraphic" />
403
    </filter>
404
    <filter
405
       id="filter2902"
406
       inkscape:label="Drop Shadow"
407
       style="color-interpolation-filters:sRGB;">
408
      <feFlood
409
         id="feFlood2892"
410
         result="flood"
411
         flood-color="rgb(0,0,0)"
412
         flood-opacity="0.498039" />
413
      <feComposite
414
         id="feComposite2894"
415
         result="composite1"
416
         operator="in"
417
         in2="SourceGraphic"
418
         in="flood" />
419
      <feGaussianBlur
420
         id="feGaussianBlur2896"
421
         result="blur"
422
         stdDeviation="2"
423
         in="composite1" />
424
      <feOffset
425
         id="feOffset2898"
426
         result="offset"
427
         dy="3"
428
         dx="3" />
429
      <feComposite
430
         id="feComposite2900"
431
         result="composite2"
432
         operator="over"
433
         in2="offset"
434
         in="SourceGraphic" />
435
    </filter>
436
    <filter
437
       id="filter2914"
438
       inkscape:label="Drop Shadow"
439
       style="color-interpolation-filters:sRGB;">
440
      <feFlood
441
         id="feFlood2904"
442
         result="flood"
443
         flood-color="rgb(0,0,0)"
444
         flood-opacity="0.498039" />
445
      <feComposite
446
         id="feComposite2906"
447
         result="composite1"
448
         operator="in"
449
         in2="SourceGraphic"
450
         in="flood" />
451
      <feGaussianBlur
452
         id="feGaussianBlur2908"
453
         result="blur"
454
         stdDeviation="2"
455
         in="composite1" />
456
      <feOffset
457
         id="feOffset2910"
458
         result="offset"
459
         dy="3"
460
         dx="3" />
461
      <feComposite
462
         id="feComposite2912"
463
         result="composite2"
464
         operator="over"
465
         in2="offset"
466
         in="SourceGraphic" />
467
    </filter>
468
    <filter
469
       id="filter2926"
470
       inkscape:label="Drop Shadow"
471
       style="color-interpolation-filters:sRGB;">
472
      <feFlood
473
         id="feFlood2916"
474
         result="flood"
475
         flood-color="rgb(0,0,0)"
476
         flood-opacity="0.498039" />
477
      <feComposite
478
         id="feComposite2918"
479
         result="composite1"
480
         operator="in"
481
         in2="SourceGraphic"
482
         in="flood" />
483
      <feGaussianBlur
484
         id="feGaussianBlur2920"
485
         result="blur"
486
         stdDeviation="2"
487
         in="composite1" />
488
      <feOffset
489
         id="feOffset2922"
490
         result="offset"
491
         dy="3"
492
         dx="3" />
493
      <feComposite
494
         id="feComposite2924"
495
         result="composite2"
496
         operator="over"
497
         in2="offset"
498
         in="SourceGraphic" />
499
    </filter>
500
    <filter
501
       id="filter2938"
502
       inkscape:label="Drop Shadow"
503
       style="color-interpolation-filters:sRGB;">
504
      <feFlood
505
         id="feFlood2928"
506
         result="flood"
507
         flood-color="rgb(0,0,0)"
508
         flood-opacity="0.498039" />
509
      <feComposite
510
         id="feComposite2930"
511
         result="composite1"
512
         operator="in"
513
         in2="SourceGraphic"
514
         in="flood" />
515
      <feGaussianBlur
516
         id="feGaussianBlur2932"
517
         result="blur"
518
         stdDeviation="2"
519
         in="composite1" />
520
      <feOffset
521
         id="feOffset2934"
522
         result="offset"
523
         dy="3"
524
         dx="3" />
525
      <feComposite
526
         id="feComposite2936"
527
         result="composite2"
528
         operator="over"
529
         in2="offset"
530
         in="SourceGraphic" />
531
    </filter>
532
    <filter
533
       id="filter2950"
534
       inkscape:label="Drop Shadow"
535
       style="color-interpolation-filters:sRGB;">
536
      <feFlood
537
         id="feFlood2940"
538
         result="flood"
539
         flood-color="rgb(0,0,0)"
540
         flood-opacity="0.498039" />
541
      <feComposite
542
         id="feComposite2942"
543
         result="composite1"
544
         operator="in"
545
         in2="SourceGraphic"
546
         in="flood" />
547
      <feGaussianBlur
548
         id="feGaussianBlur2944"
549
         result="blur"
550
         stdDeviation="2"
551
         in="composite1" />
552
      <feOffset
553
         id="feOffset2946"
554
         result="offset"
555
         dy="3"
556
         dx="3" />
557
      <feComposite
558
         id="feComposite2948"
559
         result="composite2"
560
         operator="over"
561
         in2="offset"
562
         in="SourceGraphic" />
563
    </filter>
564
    <filter
565
       id="filter2962"
566
       inkscape:label="Drop Shadow"
567
       style="color-interpolation-filters:sRGB;">
568
      <feFlood
569
         id="feFlood2952"
570
         result="flood"
571
         flood-color="rgb(0,0,0)"
572
         flood-opacity="0.498039" />
573
      <feComposite
574
         id="feComposite2954"
575
         result="composite1"
576
         operator="in"
577
         in2="SourceGraphic"
578
         in="flood" />
579
      <feGaussianBlur
580
         id="feGaussianBlur2956"
581
         result="blur"
582
         stdDeviation="2"
583
         in="composite1" />
584
      <feOffset
585
         id="feOffset2958"
586
         result="offset"
587
         dy="3"
588
         dx="3" />
589
      <feComposite
590
         id="feComposite2960"
591
         result="composite2"
592
         operator="over"
593
         in2="offset"
594
         in="SourceGraphic" />
595
    </filter>
596
    <filter
597
       id="filter2974"
598
       inkscape:label="Drop Shadow"
599
       style="color-interpolation-filters:sRGB;">
600
      <feFlood
601
         id="feFlood2964"
602
         result="flood"
603
         flood-color="rgb(0,0,0)"
604
         flood-opacity="0.498039" />
605
      <feComposite
606
         id="feComposite2966"
607
         result="composite1"
608
         operator="in"
609
         in2="SourceGraphic"
610
         in="flood" />
611
      <feGaussianBlur
612
         id="feGaussianBlur2968"
613
         result="blur"
614
         stdDeviation="2"
615
         in="composite1" />
616
      <feOffset
617
         id="feOffset2970"
618
         result="offset"
619
         dy="3"
620
         dx="3" />
621
      <feComposite
622
         id="feComposite2972"
623
         result="composite2"
624
         operator="over"
625
         in2="offset"
626
         in="SourceGraphic" />
627
    </filter>
628
    <filter
629
       id="filter2986"
630
       inkscape:label="Drop Shadow"
631
       style="color-interpolation-filters:sRGB;">
632
      <feFlood
633
         id="feFlood2976"
634
         result="flood"
635
         flood-color="rgb(0,0,0)"
636
         flood-opacity="0.498039" />
637
      <feComposite
638
         id="feComposite2978"
639
         result="composite1"
640
         operator="in"
641
         in2="SourceGraphic"
642
         in="flood" />
643
      <feGaussianBlur
644
         id="feGaussianBlur2980"
645
         result="blur"
646
         stdDeviation="2"
647
         in="composite1" />
648
      <feOffset
649
         id="feOffset2982"
650
         result="offset"
651
         dy="3"
652
         dx="3" />
653
      <feComposite
654
         id="feComposite2984"
655
         result="composite2"
656
         operator="over"
657
         in2="offset"
658
         in="SourceGraphic" />
659
    </filter>
660
  </defs>
661
  <sodipodi:namedview
662
     inkscape:snap-text-baseline="false"
663
     inkscape:document-rotation="0"
664
     fit-margin-bottom="20"
665
     fit-margin-right="20"
666
     fit-margin-left="20"
667
     fit-margin-top="20"
668
     inkscape:current-layer="svg4610"
669
     inkscape:cy="370.55742"
670
     inkscape:cx="398.61418"
671
     inkscape:zoom="1.3753763"
672
     showgrid="false"
673
     id="namedview4612"
674
     inkscape:window-height="1280"
675
     inkscape:window-width="2055"
676
     inkscape:pageshadow="2"
677
     inkscape:pageopacity="1"
678
     guidetolerance="10"
679
     gridtolerance="10"
680
     objecttolerance="10"
681
     borderopacity="1"
682
     bordercolor="#666666"
683
     pagecolor="#ffffff"
684
     inkscape:window-x="215"
685
     inkscape:window-y="26"
686
     inkscape:window-maximized="0" />
687
  <path
688
     sodipodi:nodetypes="ccssssc"
689
     inkscape:connector-curvature="0"
690
     style="fill:#333333;fill-opacity:0.0666667;fill-rule:nonzero;stroke:#df4d65;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
691
     d="M 53.35547,445.11522 V 790.96744 H 741.0332 c 1.6112,0 2.90821,-1.29701 2.90821,-2.9082 V 448.02342 c 0,-1.6112 -1.297,-2.9082 -2.90821,-2.9082 z"
692
     id="path9961" />
693
  <path
694
     sodipodi:nodetypes="sssccssss"
695
     id="path9940"
696
     d="m 20.5,787.82486 c 0,0.87013 0.35019,1.65683 0.91797,2.22461 0.56778,0.56778 1.35253,0.91797 2.22265,0.91797 H 53.35547 V 445.11522 H 23.64062 c -0.87012,0 -1.65487,0.35019 -2.22265,0.91797 -0.56778,0.56778 -0.91797,1.35254 -0.91797,2.22266 z"
697
     style="fill:#df4d65;fill-opacity:1;fill-rule:nonzero;stroke:#df4d65;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
698
     inkscape:connector-curvature="0" />
699
  <path
700
     sodipodi:nodetypes="sssccssss"
701
     id="path11125"
702
     d="m 20.5,423.31014 c 0,0.87013 0.35019,1.65683 0.91797,2.22461 0.56778,0.56778 1.354494,0.9764 2.22265,0.91797 H 53.35547 V 210.6005 H 23.64062 c -0.87012,0 -1.65487,0.3502 -2.22265,0.918 C 20.85019,212.08629 20.5,212.871 20.5,213.74109 Z"
703
     style="fill:#3e3e3e;fill-opacity:1;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
704
     inkscape:connector-curvature="0" />
705
  <path
706
     sodipodi:nodetypes="ccssssc"
707
     inkscape:connector-curvature="0"
708
     style="fill:#333333;fill-opacity:0.0666667;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
709
     d="m 53.35547,210.6005 v 215.85222 h 687.67774 c 1.6112,0 2.9082,-1.29701 2.9082,-2.9082 V 213.5087 c 0,-1.6112 -1.29701,-2.90352 -2.9082,-2.9082 z"
710
     id="path11123" />
711
  <path
712
     id="path6150"
713
     d="m 557.756,222.53493 c -0.87012,0 -1.65683,0.35019 -2.22461,0.91797 -0.56778,0.56778 -0.91797,1.35253 -0.91797,2.22265 v 29.71485 h 165.6211 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
714
     style="fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2926)"
715
     inkscape:connector-curvature="0"
716
     sodipodi:nodetypes="sssccssss" />
717
  <path
718
     sodipodi:nodetypes="ccssssc"
719
     id="path6134"
720
     d="m 720.75716,255.39041 h -165.6211 v 152.63392 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 h 159.80469 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z"
721
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2926)"
722
     inkscape:connector-curvature="0" />
723
  <path
724
     id="path6082"
725
     d="m 317.13559,222.53494 c -0.87011,0 -1.65683,0.35019 -2.2246,0.91797 -0.56779,0.56778 -0.91798,1.35253 -0.91798,2.22265 v 29.71485 h 165.62111 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91798,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
726
     style="fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2938)"
727
     inkscape:connector-curvature="0"
728
     sodipodi:nodetypes="sssccssss" />
729
  <path
730
     sodipodi:nodetypes="ccssssc"
731
     id="path6080"
732
     d="M 479.61412,255.39041 H 313.99301 v 152.63392 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 h 159.80469 c 1.6112,0 2.90821,-1.29701 2.90821,-2.90821 z"
733
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2938)"
734
     inkscape:connector-curvature="0" />
735
  <path
736
     id="path10980"
737
     d="M 53.35547,20.500012 V 188.35224 h 687.67774 c 1.6112,0 2.9082,-1.29701 2.9082,-2.9082 V 23.408212 c 0,-1.6112 -1.29701,-2.912886 -2.9082,-2.9082 z"
738
     style="fill:#333333;fill-opacity:0.0666667;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
739
     inkscape:connector-curvature="0"
740
     sodipodi:nodetypes="ccssssc" />
741
  <path
742
     inkscape:connector-curvature="0"
743
     style="fill:#3e3e3e;fill-opacity:1;fill-rule:nonzero;stroke:#3e3e3e;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
744
     d="m 20.5,185.20966 c 0,0.87013 0.35019,1.65683 0.91797,2.22461 0.56778,0.56778 1.35253,0.91797 2.22265,0.91797 H 53.35547 V 20.500012 H 23.64062 c -0.87012,0 -1.65487,0.350201 -2.22265,0.918 -0.56778,0.5678 -0.91797,1.3525 -0.91797,2.2226 z"
745
     id="path10982"
746
     sodipodi:nodetypes="sssccssss" />
747
  <path
748
     sodipodi:nodetypes="sssccssss"
749
     inkscape:connector-curvature="0"
750
     style="fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2962)"
751
     d="m 557.75599,36.704447 c -0.87012,0 -1.65683,0.35019 -2.22461,0.91797 -0.56778,0.56778 -0.91797,1.35253 -0.91797,2.22265 v 29.71485 h 165.6211 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
752
     id="path4857" />
753
  <path
754
     inkscape:connector-curvature="0"
755
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2962)"
756
     d="M 720.23451,69.559917 H 554.61341 V 169.2396 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 h 159.80469 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z"
757
     id="path4853" />
758
  <path
759
     sodipodi:nodetypes="sssccssss"
760
     inkscape:connector-curvature="0"
761
     style="fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2974)"
762
     d="m 317.13558,36.704447 c -0.87011,0 -1.65683,0.35019 -2.2246,0.91797 -0.56779,0.56778 -0.91798,1.35253 -0.91798,2.22265 v 29.71485 h 165.62111 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91798,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
763
     id="path5726" />
764
  <path
765
     inkscape:connector-curvature="0"
766
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2974)"
767
     d="M 479.61411,69.559917 H 313.993 V 169.2396 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 H 476.7059 c 1.6112,0 2.90821,-1.29701 2.90821,-2.90821 z"
768
     id="path5724" />
769
  <path
770
     id="path4721"
771
     d="m 235.85308,44.704447 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z"
772
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
773
     inkscape:connector-curvature="0" />
774
  <path
775
     sodipodi:nodetypes="sssccssss"
776
     id="path4719"
777
     d="m 76.515197,36.704447 c -0.870125,0 -1.656831,0.35019 -2.22461,0.91797 -0.567778,0.56778 -0.917968,1.35253 -0.917968,2.22265 v 29.71485 H 238.99371 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
778
     style="fill:#46c7f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2986)"
779
     inkscape:connector-curvature="0" />
780
  <path
781
     id="path4723"
782
     d="M 238.99372,69.559917 H 73.372613 V 169.2396 c 0,1.6112 1.29701,2.90821 2.90821,2.90821 H 236.08552 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z"
783
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2986)"
784
     inkscape:connector-curvature="0" />
785
  <path
786
     id="rect4622"
787
     d="m 76.280822,44.704447 c -1.611195,0 -2.908203,1.297 -2.908203,2.9082 v 0.23242 c 0,-0.87012 0.35019,-1.65487 0.917968,-2.22265 0.567779,-0.56778 1.354485,-0.91797 2.22461,-0.91797 z"
788
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
789
     inkscape:connector-curvature="0" />
790
  <path
791
     sodipodi:nodetypes="cc"
792
     inkscape:connector-curvature="0"
793
     id="path9889"
794
     d="m 397.61301,500.62068 -0.50618,32.59418"
795
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:1.9694221;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9893)" />
796
  <path
797
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9715)"
798
     d="m 554.61351,648.83688 -69.6817,47.69253"
799
     id="path9711"
800
     inkscape:connector-curvature="0"
801
     sodipodi:nodetypes="cc" />
802
  <path
803
     sodipodi:nodetypes="cc"
804
     inkscape:connector-curvature="0"
805
     id="path9675"
806
     d="M 554.61351,567.95047 484.93181,615.643"
807
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9679)" />
808
  <rect
809
     ry="3.9839513"
810
     rx="3.9205718"
811
     y="537.09552"
812
     x="554.61353"
813
     height="32.855"
814
     width="165.621"
815
     id="rect9618"
816
     style="opacity:1;fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2842)" />
817
  <rect
818
     ry="3.9839513"
819
     rx="3.9205718"
820
     y="537.09552"
821
     x="73.372665"
822
     height="32.855"
823
     width="165.621"
824
     id="rect9614"
825
     style="opacity:1;fill:#46c7f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2914)" />
826
  <path
827
     inkscape:connector-curvature="0"
828
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
829
     d="m 235.85308,545.09525 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z"
830
     id="path9323" />
831
  <path
832
     inkscape:connector-curvature="0"
833
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
834
     d="m 76.280823,545.09525 c -1.611195,0 -2.908203,1.297 -2.908203,2.9082 v 0.23242 c 0,-0.87012 0.35019,-1.65487 0.917968,-2.22265 0.567779,-0.56778 1.354485,-0.91797 2.22461,-0.91797 z"
835
     id="path9327" />
836
  <rect
837
     style="opacity:1;fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2902)"
838
     id="rect9616"
839
     width="165.621"
840
     height="32.855"
841
     x="313.99307"
842
     y="537.09552"
843
     rx="3.9205718"
844
     ry="3.9839513" />
845
  <path
846
     sodipodi:nodetypes="cc"
847
     inkscape:connector-curvature="0"
848
     id="path9491"
849
     d="m 240.99257,554.11276 65.23376,-1.01307"
850
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9513)" />
851
  <path
852
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9640)"
853
     d="m 481.61298,554.11276 65.23376,-1.01307"
854
     id="path9501"
855
     inkscape:connector-curvature="0"
856
     sodipodi:nodetypes="cc" />
857
  <rect
858
     ry="3.9839513"
859
     rx="3.9205718"
860
     y="617.79578"
861
     x="313.99307"
862
     height="32.855"
863
     width="165.621"
864
     id="rect9620"
865
     style="opacity:1;fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2890)" />
866
  <path
867
     sodipodi:nodetypes="cc"
868
     inkscape:connector-curvature="0"
869
     id="path9681"
870
     d="m 481.61298,634.81299 65.23376,-1.01307"
871
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9685)" />
872
  <rect
873
     style="opacity:1;fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2854)"
874
     id="rect9687"
875
     width="165.621"
876
     height="32.855"
877
     x="554.61353"
878
     y="617.79578"
879
     rx="3.9205718"
880
     ry="3.9839513" />
881
  <path
882
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9750)"
883
     d="m 481.61298,715.51321 65.23376,-1.01307"
884
     id="path9734"
885
     inkscape:connector-curvature="0"
886
     sodipodi:nodetypes="cc" />
887
  <rect
888
     ry="3.9839513"
889
     rx="3.9205718"
890
     y="698.49591"
891
     x="554.61353"
892
     height="32.855"
893
     width="165.621"
894
     id="rect9736"
895
     style="opacity:1;fill:#c53bd7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2866)" />
896
  <path
897
     id="path9830"
898
     d="m 356.40451,489.45323 c -0.80426,0 -1.45167,0.64741 -1.45167,1.45166 v 0.11602 c 0,-0.43433 0.1748,-0.82605 0.45822,-1.10946 0.28341,-0.28342 0.6761,-0.45822 1.11043,-0.45822 z"
899
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.10902636;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
900
     inkscape:connector-curvature="0" />
901
  <rect
902
     style="opacity:1;fill:#ffb73a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.9391377;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
903
     id="rect9826"
904
     width="120.98324"
905
     height="24"
906
     x="336.82672"
907
     y="477.86002"
908
     rx="2.8639088"
909
     ry="2.9102066" />
910
  <path
911
     id="path10514"
912
     d="m 235.85301,637.23875 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z"
913
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
914
     inkscape:connector-curvature="0" />
915
  <rect
916
     style="opacity:1;fill:#3dd092;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:4.02355671;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2878)"
917
     id="rect9717"
918
     width="165.621"
919
     height="32.855"
920
     x="313.99307"
921
     y="698.49591"
922
     rx="3.9205718"
923
     ry="3.9839513" />
924
  <path
925
     id="path10537"
926
     d="M 238.99366,636.97465 H 73.372671 V 729.175 c 0,1.2055 0.970418,2.17592 2.175911,2.17592 H 236.81776 c 1.20549,0 2.1759,-0.97042 2.1759,-2.17592 z"
927
     style="opacity:1;fill:#333333;fill-opacity:0.93333333;fill-rule:nonzero;stroke:none;stroke-width:0.16342013;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
928
     inkscape:connector-curvature="0"
929
     sodipodi:nodetypes="ccssssc" />
930
  <path
931
     sodipodi:nodetypes="sssccssss"
932
     id="path10516"
933
     d="m 75.723937,612.39226 c -0.651025,0 -1.239637,0.26201 -1.664447,0.68682 -0.424811,0.42482 -0.686822,1.01196 -0.686822,1.66299 v 22.23258 H 238.99366 v -22.23258 c 0,-0.65103 -0.26201,-1.23817 -0.68682,-1.66299 -0.42481,-0.42481 -1.01197,-0.68682 -1.66299,-0.68682 z"
934
     style="opacity:1;fill:#ffb73a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.16342013;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
935
     inkscape:connector-curvature="0" />
936
  <path
937
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker10933)"
938
     d="m 240.99257,715.51318 65.23376,-1.01307"
939
     id="path10929"
940
     inkscape:connector-curvature="0"
941
     sodipodi:nodetypes="cc" />
942
  <path
943
     style="fill:#df4d65;fill-opacity:1;fill-rule:evenodd;stroke-width:0.05250736"
944
     d="m 399.47357,99.155037 -0.12716,0.0127 -0.12716,0.0141 -0.12574,0.0141 -0.12716,0.0155 -0.61883,0.967813 -0.29106,0.9325 -0.17943,0.91977 -0.1215,1.09073 -0.30943,0.0636 -0.308,0.0678 -0.308,0.0735 -0.30801,0.0763 -0.58069,-0.93108 -0.5609,-0.75023 -0.66687,-0.71349 -0.97912,-0.6033 -0.12009,0.0409 -0.1201,0.0424 -0.12009,0.0424 -0.12009,0.0424 -0.38854,1.08085 -0.0763,0.97487 0.0297,0.93673 0.12575,1.08932 -0.28823,0.1314 -0.28681,0.13563 -0.28398,0.13987 -0.28117,0.14271 -0.77425,-0.77849 -0.7149,-0.60612 -0.80816,-0.54819 -1.0879,-0.37017 -0.10879,0.0678 -0.10738,0.0679 -0.10738,0.0678 -0.10738,0.0678 -0.13846,1.14018 0.14129,0.9664 0.23735,0.90706 0.36594,1.03563 -0.25149,0.19215 -0.24865,0.19497 -0.24585,0.19922 -0.24442,0.20204 -0.92825,-0.58634 -0.83077,-0.43234 -0.90988,-0.35463 -1.14301,-0.11868 -0.0904,0.0904 -0.0904,0.089 -0.089,0.0904 -0.0904,0.0904 0.11868,1.14301 0.35462,0.90988 0.43234,0.83218 0.58634,0.92684 -0.20204,0.24442 -0.19921,0.24584 -0.19498,0.24867 -0.19215,0.25148 -1.03563,-0.36593 -0.90705,-0.23736 -0.96641,-0.14128 -1.14018,0.13845 -0.0678,0.10738 -0.0678,0.10738 -0.0678,0.10738 -0.0678,0.10879 0.37016,1.0879 0.5482,0.80816 0.60612,0.71491 0.77848,0.77425 -0.1427,0.28117 -0.13987,0.28398 -0.13564,0.28681 -0.13139,0.28823 -1.09073,-0.12575 -0.93532,-0.0297 -0.97487,0.0763 -1.08084,0.38854 -0.0424,0.12009 -0.0424,0.12009 -0.0424,0.1201 -0.0409,0.12009 0.6033,0.97912 0.7135,0.66686 0.75023,0.56091 0.93107,0.58069 -0.0763,0.30801 -0.0735,0.308 -0.0678,0.308 -0.0636,0.30942 -1.09073,0.1215 -0.91977,0.17944 -0.9325,0.29105 -0.96781,0.61883 -0.0156,0.12717 -0.0141,0.12574 -0.0141,0.12716 -0.0127,0.12716 0.80533,0.81804 0.84348,0.49168 0.85619,0.38006 1.03704,0.36028 -0.006,0.31648 -0.003,0.31648 0.003,0.31648 0.006,0.31648 -1.03704,0.35887 -0.85619,0.38006 -0.84348,0.49168 -0.80533,0.81946 0.0127,0.12716 0.0141,0.12574 0.0141,0.12716 0.0156,0.12574 0.96781,0.62026 0.9325,0.29104 0.91977,0.17944 1.09073,0.12009 0.0636,0.30942 0.0678,0.30941 0.0735,0.30801 0.0763,0.30659 -0.93107,0.5821 -0.75023,0.56091 -0.7135,0.66687 -0.6033,0.97771 0.0409,0.12008 0.0424,0.12151 0.0424,0.1201 0.0424,0.11868 1.08084,0.38995 0.97487,0.0763 0.93532,-0.0297 1.09073,-0.12574 0.13139,0.28822 0.13564,0.2854 0.13987,0.28399 0.1427,0.28257 -0.77848,0.77425 -0.60612,0.7135 -0.5482,0.80957 -0.37016,1.08791 0.0678,0.10737 0.0678,0.10879 0.0678,0.10738 0.0678,0.10738 1.14018,0.13846 0.96641,-0.1427 0.90705,-0.23736 1.03563,-0.36452 0.19215,0.25149 0.19498,0.24866 0.19921,0.24584 0.20204,0.24302 -0.58634,0.92825 -0.43234,0.83076 -0.35462,0.9113 -0.11868,1.14159 0.0904,0.0918 0.089,0.089 0.0904,0.0904 0.0904,0.089 1.14301,-0.11868 0.90988,-0.35321 0.83077,-0.43375 0.92825,-0.58493 0.24442,0.20204 0.24585,0.19921 0.24865,0.19497 0.25149,0.19216 -0.36594,1.03563 -0.23735,0.90564 -0.14129,0.9664 0.13846,1.14018 0.10738,0.0692 0.10738,0.0678 0.10738,0.0678 0.10879,0.0664 1.0879,-0.37017 0.80816,-0.54677 0.7149,-0.60754 0.77425,-0.77708 0.28117,0.14271 0.28398,0.13987 0.28681,0.13422 0.28823,0.13139 -0.12575,1.09074 -0.0297,0.93673 0.0763,0.97346 0.38854,1.08084 0.12009,0.0438 0.12009,0.0424 0.1201,0.041 0.12009,0.0409 0.97912,-0.60188 0.66687,-0.71349 0.5609,-0.75165 0.58069,-0.93108 0.30801,0.0777 0.308,0.072 0.308,0.0692 0.30943,0.0636 0.1215,1.09073 0.17943,0.91978 0.29106,0.93249 0.61883,0.9664 0.12716,0.0156 0.12574,0.0141 0.12716,0.0141 0.12716,0.0141 0.81806,-0.80533 0.49167,-0.8449 0.38006,-0.85619 0.36028,-1.03704 0.31648,0.007 0.31648,0.003 0.31649,-0.003 0.31648,-0.007 0.36028,1.03704 0.37865,0.85619 0.49167,0.8449 0.81947,0.80533 0.12715,-0.0141 0.12574,-0.0141 0.12717,-0.0141 0.12574,-0.0156 0.62025,-0.9664 0.29104,-0.93249 0.17944,-0.91978 0.12009,-1.09073 0.30942,-0.0636 0.30942,-0.0692 0.30799,-0.072 0.3066,-0.0777 0.58211,0.93108 0.5609,0.75165 0.66687,0.71349 0.97771,0.60188 0.12009,-0.0409 0.1215,-0.041 0.1201,-0.0424 0.11868,-0.0438 0.38995,-1.08084 0.0763,-0.97346 -0.0297,-0.93673 -0.12574,-1.09074 0.28822,-0.13139 0.2854,-0.13422 0.28398,-0.13987 0.28258,-0.14271 0.77424,0.77708 0.7135,0.60754 0.80957,0.54677 1.08791,0.37017 0.10737,-0.0664 0.10879,-0.0678 0.10738,-0.0678 0.10738,-0.0692 0.13847,-1.14018 -0.14271,-0.9664 -0.23737,-0.90564 -0.36452,-1.03563 0.25149,-0.19216 0.24866,-0.19497 0.24585,-0.19921 0.24301,-0.20204 0.92825,0.58493 0.83077,0.43375 0.91129,0.35321 1.1416,0.11868 0.0904,-0.089 0.0904,-0.0904 0.0904,-0.089 0.089,-0.0918 -0.11868,-1.14159 -0.35321,-0.9113 -0.43375,-0.83076 -0.58492,-0.92825 0.20203,-0.24302 0.19921,-0.24584 0.19498,-0.24866 0.19215,-0.25149 1.03563,0.36452 0.90564,0.23736 0.9664,0.1427 1.14018,-0.13846 0.0692,-0.10738 0.0678,-0.10738 0.0678,-0.10879 0.0664,-0.10737 -0.37017,-1.08791 -0.54677,-0.80957 -0.60754,-0.7135 -0.77706,-0.77425 0.1427,-0.28257 0.13986,-0.28399 0.13423,-0.2854 0.13139,-0.28822 1.09073,0.12574 0.93674,0.0297 0.97345,-0.0763 1.08085,-0.38995 0.0438,-0.11868 0.0424,-0.1201 0.0409,-0.12151 0.041,-0.12008 -0.6019,-0.97771 -0.71349,-0.66687 -0.75164,-0.56091 -0.93108,-0.5821 0.0777,-0.30659 0.072,-0.30801 0.0692,-0.30941 0.0636,-0.30942 1.09073,-0.12009 0.91978,-0.17944 0.93249,-0.29104 0.9664,-0.62026 0.0155,-0.12574 0.0141,-0.12716 0.0141,-0.12574 0.0141,-0.12716 -0.80533,-0.81946 -0.8449,-0.49168 -0.85619,-0.38006 -1.03704,-0.35887 0.007,-0.31648 0.003,-0.31648 -0.003,-0.31648 -0.007,-0.31648 1.03704,-0.36028 0.85619,-0.38006 0.8449,-0.49168 0.80533,-0.81804 -0.0141,-0.12716 -0.0141,-0.12716 -0.0141,-0.12574 -0.0155,-0.12717 -0.9664,-0.61883 -0.93249,-0.29105 -0.91978,-0.17944 -1.09073,-0.1215 -0.0636,-0.30942 -0.0692,-0.308 -0.072,-0.308 -0.0777,-0.30801 0.93108,-0.58069 0.75164,-0.56091 0.71349,-0.66686 0.6019,-0.97912 -0.041,-0.12009 -0.0409,-0.1201 -0.0424,-0.12009 -0.0438,-0.12009 -1.08085,-0.38854 -0.97345,-0.0763 -0.93674,0.0297 -1.09073,0.12575 -0.13139,-0.28823 -0.13423,-0.28681 -0.13986,-0.28398 -0.1427,-0.28117 0.77706,-0.77425 0.60754,-0.71491 0.54677,-0.80816 0.37017,-1.0879 -0.0664,-0.10879 -0.0678,-0.10738 -0.0678,-0.10738 -0.0692,-0.10738 -1.14018,-0.13845 -0.9664,0.14128 -0.90564,0.23736 -1.03563,0.36593 -0.19215,-0.25148 -0.19498,-0.24867 -0.19921,-0.24584 -0.20203,-0.24442 0.58492,-0.92684 0.43375,-0.83218 0.35321,-0.90988 0.11868,-1.14301 -0.089,-0.0904 -0.0904,-0.0904 -0.0904,-0.089 -0.0904,-0.0904 -1.1416,0.11868 -0.91129,0.35463 -0.83077,0.43234 -0.92825,0.58634 -0.24301,-0.20204 -0.24585,-0.19922 -0.24866,-0.19497 -0.25149,-0.19215 0.36452,-1.03563 0.23737,-0.90706 0.14271,-0.9664 -0.13847,-1.14018 -0.10738,-0.0678 -0.10738,-0.0678 -0.10879,-0.0679 -0.10737,-0.0678 -1.08791,0.37017 -0.80957,0.54819 -0.7135,0.60612 -0.77424,0.77849 -0.28258,-0.14271 -0.28398,-0.13987 -0.2854,-0.13563 -0.28822,-0.1314 0.12574,-1.08932 0.0297,-0.93673 -0.0763,-0.97487 -0.38995,-1.08085 -0.11868,-0.0424 -0.1201,-0.0424 -0.1215,-0.0424 -0.12009,-0.0409 -0.97771,0.6033 -0.66687,0.71349 -0.5609,0.75023 -0.58211,0.93108 -0.3066,-0.0763 -0.30799,-0.0735 -0.30942,-0.0678 -0.30942,-0.0636 -0.12009,-1.09073 -0.17944,-0.91977 -0.29104,-0.9325 -0.62025,-0.967813 -0.12574,-0.0155 -0.12717,-0.0141 -0.12574,-0.0141 -0.12715,-0.0127 -0.81947,0.80533 -0.49167,0.843483 -0.37865,0.8562 -0.36028,1.03704 -0.31648,-0.006 -0.31649,-0.003 -0.31648,0.003 -0.31648,0.006 -0.36028,-1.03704 -0.38006,-0.8562 -0.49167,-0.843483 z m 2.68302,20.688573 a 5.3990039,5.3990039 0 0 1 5.39856,5.39997 5.3990039,5.3990039 0 0 1 -5.39856,5.39855 5.3990039,5.3990039 0 0 1 -5.39996,-5.39855 5.3990039,5.3990039 0 0 1 5.39996,-5.39997 z"
945
     id="path5693"
946
     inkscape:connector-curvature="0" />
947
  <path
948
     inkscape:connector-curvature="0"
949
     d="m 380.9529,101.31918 a 4.37599,4.37599 0 0 1 -4.37599,4.37599 4.37599,4.37599 0 0 1 -4.37599,-4.37599 4.37599,4.37599 0 0 1 4.37599,-4.375983 4.37599,4.37599 0 0 1 4.37599,4.375983 z m 4.63493,-1.27213 c -0.32212,-0.118873 -0.95326,0.0926 -0.92258,-0.401293 -0.13877,-0.39635 -0.21401,-0.74537 0.27363,-0.88946 0.78055,-0.47633 1.45123,-1.16128 1.74461,-2.04171 0.15411,-0.39145 -0.3432,-0.48754 -0.63657,-0.53536 -0.91614,-0.25589 -1.86519,0.0578 -2.73328,0.35995 -0.11023,-0.31345 -0.69059,-0.56868 -0.47901,-0.88657 0.56405,-0.84324 0.99162,-1.8335 0.85012,-2.86709 -0.031,-0.41955 -0.52073,-0.29038 -0.8058,-0.20618 -0.93646,0.16692 -1.65537,0.86143 -2.30642,1.51029 -0.23528,-0.23464 -0.86896,-0.21274 -0.81624,-0.59097 0.14241,-1.00446 0.0978,-2.08217 -0.47806,-2.95201 -0.20995,-0.36459 -0.59514,-0.0357 -0.81544,0.16385 -0.7713,0.55671 -1.11768,1.49434 -1.42274,2.36143 -0.3138,-0.10927 -0.87519,0.18536 -0.99181,-0.17826 -0.30757,-0.96675 -0.81528,-1.91841 -1.71153,-2.45229 -0.34734,-0.23737 -0.55176,0.22606 -0.66364,0.50149 -0.45331,0.83622 -0.35865,1.83127 -0.25723,2.74482 -0.33012,0.0378 -0.70817,0.54672 -0.97098,0.26977 -0.69651,-0.73763 -1.56687,-1.37476 -2.60601,-1.46682 -0.41593,-0.0631 -0.39904,0.443 -0.38034,0.73971 -0.0456,0.95009 0.47143,1.80554 0.95918,2.58463 -0.28104,0.17725 -0.40076,0.79984 -0.75777,0.6643 -0.94759,-0.36236 -2.00818,-0.55864 -2.98437,-0.1908 -0.40215,0.12357 -0.16723,0.57227 -0.0217,0.83147 0.37114,0.8758 1.2081,1.42221 1.98561,1.91248 -0.17632,0.28163 -0.0141,0.89453 -0.39449,0.92732 -1.01097,0.0847 -2.05173,0.367983 -2.77166,1.122963 -0.3087,0.2858 0.0976,0.58816 0.34122,0.75853 0.71439,0.62804 1.70558,0.75716 2.6188,0.8616 -0.0367,0.33025 0.37548,0.81204 0.0469,1.00665 -0.87415,0.51488 -1.68886,1.22174 -2.00994,2.2143 -0.15411,0.39144 0.3432,0.48753 0.63657,0.53535 0.91612,0.25589 1.86516,-0.0578 2.73323,-0.35995 0.11029,0.31341 0.69063,0.56869 0.47905,0.88657 -0.56412,0.8432 -0.99155,1.83352 -0.85015,2.86709 0.031,0.41952 0.52075,0.2904 0.80584,0.20618 0.93644,-0.16692 1.65537,-0.86139 2.30637,-1.51029 0.2353,0.23464 0.86901,0.21272 0.81629,0.59098 -0.14241,1.00446 -0.0978,2.08215 0.47802,2.95202 0.20997,0.36455 0.59517,0.0357 0.81548,-0.16387 0.77125,-0.55674 1.11768,-1.49435 1.42274,-2.36142 0.31379,0.10926 0.8752,-0.18537 0.99181,0.17824 0.30754,0.96678 0.81527,1.91842 1.71153,2.45229 0.34733,0.23738 0.55172,-0.22608 0.66362,-0.50146 0.45335,-0.83621 0.35866,-1.83128 0.25725,-2.74484 0.33011,-0.0378 0.70812,-0.54672 0.97093,-0.26977 0.69656,0.7376 1.5669,1.37477 2.60606,1.46683 0.41593,0.0632 0.39897,-0.44304 0.38032,-0.73972 0.0457,-0.95011 -0.4715,-1.8055 -0.95916,-2.58463 0.28105,-0.17722 0.40074,-0.79983 0.75772,-0.6643 0.94761,0.36234 2.00821,0.55865 2.98442,0.1908 0.40215,-0.12357 0.16723,-0.57228 0.0217,-0.83146 -0.37116,-0.87579 -1.20814,-1.42218 -1.98561,-1.91249 0.17632,-0.28163 0.0141,-0.89453 0.39449,-0.92732 1.01097,-0.0847 2.05173,-0.36799 2.77166,-1.12295 0.3087,-0.28581 -0.0976,-0.58817 -0.34122,-0.75854 -0.47483,-0.43652 -1.13407,-0.61787 -1.75144,-0.75008 z"
950
     style="fill:#05556e;fill-opacity:1;fill-rule:evenodd;stroke-width:0.04014921"
951
     id="path5687" />
952
  <path
953
     style="fill:#05556e;fill-opacity:1;stroke-width:0.11881336"
954
     id="path4816"
955
     d="m 619.92265,90.37586 h 2.72717 v 2.71445 h 2.51374 v -2.71445 h 2.72716 v 8.21477 h -2.72716 v -2.73825 h -2.49003 v 2.73825 h -2.75088 m 11.57268,-5.47651 h -2.40702 v -2.73826 h 7.55307 v 2.73826 h -2.41888 v 5.47651 h -2.72717 m 6.34363,-8.21477 h 2.8576 l 1.75487,2.89303 1.75487,-2.89303 h 2.8576 v 8.21477 h -2.72717 v -4.07167 l -1.90901,2.95256 -1.90902,-2.95256 v 4.07167 h -2.67974 m 10.57667,-8.21477 h 2.72717 v 5.50033 h 3.86546 v 2.71444 h -6.59263"
956
     inkscape:connector-curvature="0" />
957
  <path
958
     id="path4818"
959
     d="m 619.82779,146.45062 -3.91289,-44.09786 h 43.01811 l -3.91289,44.07405 -17.63174,4.90505"
960
     inkscape:connector-curvature="0"
961
     style="fill:#e44d26;stroke-width:0.11881336" />
962
  <path
963
     id="path4820"
964
     d="m 637.42396,147.58164 v -41.60962 h 17.5843 l -3.3556,37.62129"
965
     inkscape:connector-curvature="0"
966
     style="fill:#f16529;stroke-width:0.11881336" />
967
  <path
968
     id="path4822"
969
     d="m 623.90669,111.3652 h 13.51727 v 5.40508 h -7.61236 l 0.498,5.53605 h 7.11436 v 5.39318 h -12.04697 m 0.23714,2.71444 h 5.40691 l 0.37943,4.32169 6.02349,1.61914 v 5.64319 L 626.373,138.90255"
970
     inkscape:connector-curvature="0"
971
     style="fill:#ebebeb;stroke-width:0.11881336" />
972
  <path
973
     id="path4824"
974
     d="m 650.89379,111.3652 h -13.49355 v 5.40508 h 12.99555 m -0.48615,5.53605 h -12.5094 v 5.40508 h 6.64006 l -0.62843,7.02423 -6.01163,1.61914 v 5.61938 l 11.02724,-3.07161"
975
     inkscape:connector-curvature="0"
976
     style="fill:#ffffff;stroke-width:0.11881336" />
977
  <path
978
     sodipodi:nodetypes="cc"
979
     inkscape:connector-curvature="0"
980
     id="path5804"
981
     d="m 240.99252,105.07517 65.2338,-1.01308"
982
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9479)" />
983
  <path
984
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9505)"
985
     d="m 481.61302,105.07517 65.2337,-1.01308"
986
     id="path9497"
987
     inkscape:connector-curvature="0"
988
     sodipodi:nodetypes="cc" />
989
  <path
990
     inkscape:connector-curvature="0"
991
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
992
     d="m 235.85308,230.53494 c 0.87012,0 1.65488,0.35019 2.22266,0.91797 0.56778,0.56778 0.91797,1.35253 0.91797,2.22265 v -0.23242 c 0,-1.6112 -1.297,-2.9082 -2.9082,-2.9082 z"
993
     id="path6102" />
994
  <path
995
     inkscape:connector-curvature="0"
996
     style="opacity:1;fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.21841836;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
997
     d="m 76.280823,230.53494 c -1.611195,0 -2.908203,1.297 -2.908203,2.9082 v 0.23242 c 0,-0.87012 0.35019,-1.65487 0.917968,-2.22265 0.567779,-0.56778 1.354485,-0.91797 2.22461,-0.91797 z"
998
     id="path6106" />
999
  <path
1000
     inkscape:connector-curvature="0"
1001
     style="fill:#46c7f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2950)"
1002
     d="m 76.515198,222.53494 c -0.870125,0 -1.656831,0.35019 -2.22461,0.91797 -0.567778,0.56778 -0.917968,1.35253 -0.917968,2.22265 v 29.71485 h 165.62109 v -29.71485 c 0,-0.87012 -0.35019,-1.65487 -0.91797,-2.22265 -0.56778,-0.56778 -1.35254,-0.91797 -2.22266,-0.91797 z"
1003
     id="path6104"
1004
     sodipodi:nodetypes="sssccssss" />
1005
  <path
1006
     sodipodi:nodetypes="ccssssc"
1007
     inkscape:connector-curvature="0"
1008
     style="fill:#e6e7e7;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.218418;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;filter:url(#filter2950)"
1009
     d="M 238.99371,255.39041 H 73.37262 v 152.63392 c 0,1.6112 1.297008,2.90821 2.908203,2.90821 H 236.08551 c 1.6112,0 2.9082,-1.29701 2.9082,-2.90821 z"
1010
     id="path6100" />
1011
  <path
1012
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9509)"
1013
     d="m 240.99257,328.95043 65.23376,-1.01307"
1014
     id="path9485"
1015
     inkscape:connector-curvature="0"
1016
     sodipodi:nodetypes="cc" />
1017
  <path
1018
     sodipodi:nodetypes="cc"
1019
     inkscape:connector-curvature="0"
1020
     id="path9757"
1021
     d="m 481.61298,300.08996 65.23376,-1.01307"
1022
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9761)" />
1023
  <path
1024
     style="opacity:1;vector-effect:none;fill:#05556e;fill-opacity:1;stroke:#05556e;stroke-width:3.94158769;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#marker9767)"
1025
     d="M 552.61456,372.04139 487.3808,371.02832"
1026
     id="path9763"
1027
     inkscape:connector-curvature="0"
1028
     sodipodi:nodetypes="cc" />
1029
  <text
1030
     id="text2269"
1031
     y="62.149761"
1032
     x="115.43707"
1033
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1034
     xml:space="preserve"><tspan
1035
       y="62.149761"
1036
       x="115.43707"
1037
       id="tspan2267"
1038
       sodipodi:role="line">Text Edit</tspan></text>
1039
  <text
1040
     transform="rotate(-90)"
1041
     id="text2273"
1042
     y="43.507812"
1043
     x="-132.24059"
1044
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1045
     xml:space="preserve"><tspan
1046
       y="43.507812"
1047
       x="-132.24059"
1048
       id="tspan2271"
1049
       sodipodi:role="line">Today</tspan></text>
1050
  <text
1051
     id="text2277"
1052
     y="61.540386"
1053
     x="358.88168"
1054
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1055
     xml:space="preserve"><tspan
1056
       y="61.540386"
1057
       x="358.88168"
1058
       id="tspan2275"
1059
       sodipodi:role="line">Process</tspan></text>
1060
  <text
1061
     id="text2281"
1062
     y="59.34898"
1063
     x="605.30872"
1064
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1065
     xml:space="preserve"><tspan
1066
       y="59.34898"
1067
       x="605.30872"
1068
       id="tspan2279"
1069
       sodipodi:role="line">Output</tspan></text>
1070
  <text
1071
     id="text2285"
1072
     y="245.17946"
1073
     x="605.30872"
1074
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1075
     xml:space="preserve"><tspan
1076
       y="245.17946"
1077
       x="605.30872"
1078
       id="tspan2283"
1079
       sodipodi:role="line">Output</tspan></text>
1080
  <text
1081
     id="text2289"
1082
     y="247.37088"
1083
     x="358.88168"
1084
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1085
     xml:space="preserve"><tspan
1086
       y="247.37088"
1087
       x="358.88168"
1088
       id="tspan2287"
1089
       sodipodi:role="line">Process</tspan></text>
1090
  <text
1091
     id="text2293"
1092
     y="247.98026"
1093
     x="115.43707"
1094
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1095
     xml:space="preserve"><tspan
1096
       y="247.98026"
1097
       x="115.43707"
1098
       id="tspan2291"
1099
       sodipodi:role="line">Text Edit</tspan></text>
1100
  <text
1101
     transform="rotate(-90)"
1102
     id="text2297"
1103
     y="43.630859"
1104
     x="-363.15442"
1105
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1106
     xml:space="preserve"><tspan
1107
       y="43.630859"
1108
       x="-363.15442"
1109
       id="tspan2295"
1110
       sodipodi:role="line">Proposed</tspan></text>
1111
  <text
1112
     id="text2301"
1113
     y="314.01108"
1114
     x="98.034729"
1115
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1116
     xml:space="preserve"><tspan
1117
       id="tspan2299"
1118
       sodipodi:role="line"
1119
       x="98.034729"
1120
       y="314.01108">R Markdown</tspan></text>
1121
  <text
1122
     id="text2305"
1123
     y="285.84311"
1124
     x="107.43903"
1125
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1126
     xml:space="preserve"><tspan
1127
       id="tspan2303"
1128
       sodipodi:role="line"
1129
       x="107.43903"
1130
       y="285.84311">Markdown</tspan></text>
1131
  <text
1132
     id="text2309"
1133
     y="342.91147"
1134
     x="134.3277"
1135
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1136
     xml:space="preserve"><tspan
1137
       id="tspan2307"
1138
       sodipodi:role="line"
1139
       x="134.3277"
1140
       y="342.91147">XML</tspan></text>
1141
  <text
1142
     id="text2313"
1143
     y="370.34702"
1144
     x="113.56207"
1145
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1146
     xml:space="preserve"><tspan
1147
       id="tspan2311"
1148
       sodipodi:role="line"
1149
       x="113.56207"
1150
       y="370.34702">DocBook</tspan></text>
1151
  <text
1152
     id="text2317"
1153
     y="398.51498"
1154
     x="114.3526"
1155
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1156
     xml:space="preserve"><tspan
1157
       id="tspan2315"
1158
       sodipodi:role="line"
1159
       x="114.3526"
1160
       y="398.51498">AsciiDoc</tspan></text>
1161
  <text
1162
     transform="rotate(-90)"
1163
     id="text2329"
1164
     y="43.507812"
1165
     x="-774.87335"
1166
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1167
     xml:space="preserve"><tspan
1168
       y="43.507812"
1169
       x="-774.87335"
1170
       id="tspan2327"
1171
       sodipodi:role="line">Example Processing Combination</tspan></text>
1172
  <text
1173
     id="text2333"
1174
     y="562.05426"
1175
     x="135.31207"
1176
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#f3fbfe;fill-opacity:1;stroke:none"
1177
     xml:space="preserve"><tspan
1178
       y="562.05426"
1179
       x="135.31207"
1180
       id="tspan2331"
1181
       sodipodi:role="line">XML</tspan></text>
1182
  <text
1183
     id="text2337"
1184
     y="495.6918"
1185
     x="381.64142"
1186
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#333333;fill-opacity:0.866667;stroke:none"
1187
     xml:space="preserve"><tspan
1188
       y="495.6918"
1189
       x="381.64142"
1190
       id="tspan2335"
1191
       sodipodi:role="line">XSLT</tspan></text>
1192
  <text
1193
     id="text2341"
1194
     y="562.05426"
1195
     x="323.97742"
1196
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1197
     xml:space="preserve"><tspan
1198
       y="562.05426"
1199
       x="323.97742"
1200
       id="tspan2339"
1201
       sodipodi:role="line">XSLT Processor</tspan></text>
1202
  <text
1203
     id="text2345"
1204
     y="562.54059"
1205
     x="579.27557"
1206
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1207
     xml:space="preserve"><tspan
1208
       y="562.54059"
1209
       x="579.27557"
1210
       id="tspan2343"
1211
       sodipodi:role="line">R Markdown</tspan></text>
1212
  <text
1213
     id="text2349"
1214
     y="643.24084"
1215
     x="588.75018"
1216
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1217
     xml:space="preserve"><tspan
1218
       y="643.24084"
1219
       x="588.75018"
1220
       id="tspan2347"
1221
       sodipodi:role="line">Markdown</tspan></text>
1222
  <text
1223
     id="text2353"
1224
     y="642.63147"
1225
     x="339.61023"
1226
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1227
     xml:space="preserve"><tspan
1228
       y="642.63147"
1229
       x="339.61023"
1230
       id="tspan2351"
1231
       sodipodi:role="line">R Processor</tspan></text>
1232
  <text
1233
     id="text2357"
1234
     y="722.93903"
1235
     x="318.43912"
1236
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:21.3333px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1237
     xml:space="preserve"><tspan
1238
       y="722.93903"
1239
       x="318.43912"
1240
       id="tspan2355"
1241
       sodipodi:role="line">Variable Processor</tspan></text>
1242
  <text
1243
     id="text2361"
1244
     y="723.3316"
1245
     x="604.07831"
1246
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1247
     xml:space="preserve"><tspan
1248
       y="723.3316"
1249
       x="604.07831"
1250
       id="tspan2359"
1251
       sodipodi:role="line">HTML5</tspan></text>
1252
  <text
1253
     id="text2365"
1254
     y="630.84766"
1255
     x="81.211723"
1256
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#333333;fill-opacity:0.866667;stroke:none"
1257
     xml:space="preserve"><tspan
1258
       y="630.84766"
1259
       x="81.211723"
1260
       id="tspan2363"
1261
       sodipodi:role="line">Structured Data Source</tspan></text>
1262
  <text
1263
     id="text2369"
1264
     y="756.39404"
1265
     x="215.65826"
1266
     style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1267
     xml:space="preserve"><tspan
1268
       y="756.39404"
1269
       x="215.65826"
1270
       id="tspan2367"
1271
       sodipodi:role="line">interpolated values</tspan></text>
1272
  <g
1273
     transform="translate(-0.25585322,11.831789)"
1274
     id="g2523">
1275
    <text
1276
       xml:space="preserve"
1277
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1278
       x="156.49219"
1279
       y="708.2467"
1280
       id="text2373"><tspan
1281
         sodipodi:role="line"
1282
         id="tspan2371"
1283
         x="156.49219"
1284
         y="708.2467">CSON</tspan></text>
1285
    <text
1286
       xml:space="preserve"
1287
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1288
       x="156.49219"
1289
       y="688.41504"
1290
       id="text2377"><tspan
1291
         sodipodi:role="line"
1292
         id="tspan2375"
1293
         x="156.49219"
1294
         y="688.41504">JSONNET</tspan></text>
1295
    <text
1296
       xml:space="preserve"
1297
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1298
       x="156.49219"
1299
       y="668.24695"
1300
       id="text2381"><tspan
1301
         sodipodi:role="line"
1302
         id="tspan2379"
1303
         x="156.49219"
1304
         y="668.24695">JSON5</tspan></text>
1305
    <text
1306
       xml:space="preserve"
1307
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1308
       x="156.49219"
1309
       y="648.07886"
1310
       id="text2385"><tspan
1311
         sodipodi:role="line"
1312
         id="tspan2383"
1313
         x="156.49219"
1314
         y="648.07886">JSON</tspan></text>
1315
    <text
1316
       xml:space="preserve"
1317
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1318
       x="94.110725"
1319
       y="648.41534"
1320
       id="text2389"><tspan
1321
         sodipodi:role="line"
1322
         id="tspan2387"
1323
         x="94.110725"
1324
         y="648.41534">YAML</tspan></text>
1325
    <text
1326
       xml:space="preserve"
1327
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1328
       x="94.110725"
1329
       y="668.24695"
1330
       id="text2393"><tspan
1331
         sodipodi:role="line"
1332
         id="tspan2391"
1333
         x="94.110725"
1334
         y="668.24695">TOML</tspan></text>
1335
    <text
1336
       xml:space="preserve"
1337
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:16.4059px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#ffffff;fill-opacity:0.933333;stroke:none"
1338
       x="94.110725"
1339
       y="688.41504"
1340
       id="text2397"><tspan
1341
         sodipodi:role="line"
1342
         id="tspan2395"
1343
         x="94.110725"
1344
         y="688.41504">XML</tspan></text>
1345
  </g>
1346
  <g
1347
     transform="translate(-1.2304677,-0.85937628)"
1348
     id="g2593">
1349
    <g
1350
       id="g2532">
1351
      <rect
1352
         id="rect4698"
1353
         ry="2.7292624"
1354
         y="91.740654"
1355
         x="129.16347"
1356
         height="32.205296"
1357
         width="54.039394"
1358
         style="fill:none;stroke:#05556e;stroke-width:2.72926;stroke-opacity:1" />
1359
      <path
1360
         style="fill:#05556e;fill-opacity:1;stroke-width:0.272926"
1361
         id="path4700"
1362
         d="M 135.98663,117.12279 V 98.56381 h 5.45852 l 5.45853,6.82315 5.45852,-6.82315 h 5.45853 v 18.55898 h -5.45853 v -10.64412 l -5.45852,6.82315 -5.45853,-6.82315 v 10.64412 z m 34.11578,0 -8.18779,-9.00657 h 5.45852 v -9.55241 h 5.45853 v 9.55241 h 5.45852 z"
1363
         inkscape:connector-curvature="0" />
1364
    </g>
1365
    <text
1366
       xml:space="preserve"
1367
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1368
       x="108.73981"
1369
       y="152.80437"
1370
       id="text2407"><tspan
1371
         sodipodi:role="line"
1372
         id="tspan2405"
1373
         x="108.73981"
1374
         y="152.80437">Markdown</tspan></text>
1375
  </g>
1376
  <path
1377
     inkscape:connector-curvature="0"
1378
     d="m 417.86562,272.90923 c -2.81873,0.35302 -5.58858,1.78683 -7.90222,4.10047 -1.79226,1.78682 -3.43787,4.20365 -5.01832,7.35911 -1.28173,2.56347 -2.29191,5.21927 -2.90019,7.59265 l -0.1738,0.68975 -0.68975,0.35302 c -0.96673,0.49423 -1.81398,1.01561 -2.77528,1.69993 -3.29666,2.35709 -6.15341,5.19211 -8.53222,8.46705 -0.23354,0.32586 -0.45621,0.58656 -0.49966,0.58656 -0.038,0 -0.33673,-0.0435 -0.65716,-0.0923 -0.73863,-0.11949 -3.19891,-0.13578 -4.11676,-0.0272 -3.79633,0.46164 -7.25593,1.57502 -11.41613,3.68228 -3.00339,1.5207 -4.93685,2.87304 -6.8323,4.77391 -2.37881,2.37882 -3.80176,5.01832 -4.21452,7.82076 -0.0978,0.62457 -0.0978,2.39511 0,3.0414 0.51052,3.55193 2.55804,6.94636 5.27358,8.74404 3.15003,2.08554 7.40256,2.6558 12.27424,1.65105 3.62253,-0.75492 7.20161,-2.14527 10.77526,-4.19822 3.47046,-1.99321 5.87643,-4.18193 7.57093,-6.87575 0.27155,-0.43449 0.35845,-0.52682 0.53224,-0.59199 2.79701,-1.01018 4.74677,-2.05295 6.96265,-3.72572 2.02036,-1.5207 3.43244,-2.85675 6.0991,-5.77324 0.68432,-0.74949 0.8038,-0.91785 0.84182,-1.16225 0.0326,-0.17379 0.0543,-0.20095 0.15207,-0.17922 0.51595,0.10319 2.20502,0.11948 2.94908,0.0272 2.08553,-0.25526 4.05701,-1.10251 6.01763,-2.57976 2.61778,-1.97691 5.06177,-5.27901 6.78885,-9.17853 2.59606,-5.86556 3.57908,-10.80785 3.01425,-15.19073 -0.14121,-1.12423 -0.28241,-1.74881 -0.59742,-2.71554 -0.42905,-1.29803 -1.08621,-2.55804 -1.89001,-3.62796 -0.43449,-0.57026 -1.57502,-1.70536 -2.14528,-2.12898 -1.59131,-1.17855 -3.93753,-2.13442 -6.03936,-2.46028 -0.66259,-0.10319 -2.29735,-0.14664 -2.85132,-0.0815 z m 2.44399,7.82076 c 1.94433,0.46707 3.2152,2.04751 3.5302,4.39917 0.0815,0.58656 0.0815,2.10183 0,2.7427 -0.32043,2.62864 -1.26544,5.70263 -2.61235,8.48878 -1.01561,2.10725 -1.79226,3.34011 -2.88933,4.58383 -0.32587,0.36931 -1.38493,1.31975 -1.42838,1.2763 -0.005,-0.005 0.0706,-0.34216 0.1738,-0.74406 0.24983,-0.97759 0.34215,-1.56958 0.3856,-2.41683 0.0706,-1.58044 -0.27155,-3.09571 -0.98302,-4.30684 -1.20027,-2.05295 -3.17175,-3.41072 -5.47453,-3.78547 -0.11405,-0.0163 -0.20638,-0.0489 -0.20638,-0.076 0,-0.0217 0.19552,-0.53768 0.42905,-1.15139 1.41752,-3.67684 2.66666,-5.83298 4.30142,-7.40799 1.0482,-1.01562 1.70536,-1.40665 2.73726,-1.62933 0.51596,-0.11405 1.49355,-0.0978 2.03666,0.0272 z m -10.34078,17.93885 c 0.52139,0.54311 0.56483,0.76579 0.46164,2.25933 l -0.0326,0.51596 -0.14121,-0.21725 c -0.22811,-0.34215 -0.40733,-0.72233 -0.52682,-1.1188 -0.0652,-0.20095 -0.15207,-0.43992 -0.20095,-0.53224 -0.0706,-0.13035 -0.17922,-0.91243 -0.19008,-1.34691 0,-0.11949 0.29871,0.0923 0.63,0.43991 z m -7.36997,3.01425 c 0.3856,2.28649 1.18397,4.05159 2.44941,5.40393 l 0.45078,0.47793 -0.13577,0.14664 c -0.0706,0.0815 -0.46165,0.51052 -0.86355,0.9613 -1.55328,1.73795 -2.81873,2.98167 -4.05158,3.97012 -0.41819,0.34216 -0.78208,0.61915 -0.79837,0.61915 -0.0163,0 -0.0435,-0.0923 -0.0652,-0.20638 -0.076,-0.4019 -0.46708,-1.4664 -0.8038,-2.15614 -0.54311,-1.12424 -1.14596,-2.0095 -2.08554,-3.0577 l -0.45621,-0.50509 0.41276,-0.50509 c 1.19484,-1.47182 2.92192,-3.26951 4.43177,-4.62728 0.85811,-0.76578 1.37949,-1.21656 1.39578,-1.20027 0.005,0.005 0.0597,0.315 0.11949,0.67888 z m -16.52135,9.77052 c -0.0163,0.11405 -0.0815,0.54311 -0.14664,0.9613 -0.22267,1.47182 -0.23353,3.57365 -0.0272,4.78478 0.19008,1.10251 0.57569,2.11812 1.08078,2.81873 0.27699,0.38018 0.87441,0.97759 1.22199,1.20027 l 0.23354,0.1575 -0.15207,0.12492 c -0.60285,0.48879 -2.54174,1.58044 -4.18193,2.34622 -2.4114,1.12967 -4.36659,1.7651 -6.62049,2.16157 -0.77664,0.13578 -0.99932,0.15207 -2.09096,0.15207 -0.98846,0 -1.30889,-0.0217 -1.67278,-0.0978 -1.5207,-0.33672 -2.53088,-0.97216 -3.1989,-2.0095 -0.53225,-0.82552 -0.72234,-1.48268 -0.72777,-2.43855 0,-1.56415 0.57027,-2.68296 2.17244,-4.27969 1.78682,-1.77597 3.93753,-3.05227 7.72299,-4.5784 2.01493,-0.81467 4.20366,-1.37407 5.75151,-1.4664 0.74406,-0.0434 0.66803,-0.0652 0.63544,0.16294 z m 6.13712,3.5302 c -0.0163,0.0543 -0.0272,0.0109 -0.0272,-0.0923 0,-0.10319 0.0109,-0.14664 0.0272,-0.0978 0.0109,0.0543 0.0109,0.14121 0,0.19009 z"
1379
     id="path8164"
1380
     style="fill:#df4d65;fill-opacity:1;stroke:none;stroke-width:0.00543108" />
1381
  <g
1382
     transform="translate(1.378418e-5,1.0193503)"
1383
     id="g1168">
1384
    <text
1385
       id="text1158"
1386
       y="364.17905"
1387
       x="349.05551"
1388
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1389
       xml:space="preserve"><tspan
1390
         id="tspan1156"
1391
         sodipodi:role="line"
1392
         x="349.05551"
1393
         y="364.17905">Processor</tspan></text>
1394
    <text
1395
       xml:space="preserve"
1396
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1397
       x="370.40707"
1398
       y="392.17905"
1399
       id="text1162"><tspan
1400
         y="392.17905"
1401
         x="370.40707"
1402
         sodipodi:role="line"
1403
         id="tspan1160">Chain</tspan></text>
1404
  </g>
1405
  <g
1406
     transform="translate(0,-2.3144459)"
1407
     id="g1206">
1408
    <text
1409
       xml:space="preserve"
1410
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1411
       x="586.44855"
1412
       y="327.56967"
1413
       id="text1190"><tspan
1414
         y="327.56967"
1415
         x="586.44855"
1416
         sodipodi:role="line"
1417
         id="tspan1188">Processor-</tspan></text>
1418
    <text
1419
       id="text1194"
1420
       y="355.56967"
1421
       x="588.43488"
1422
       style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:condensed;font-size:24px;line-height:1.25;font-family:'Roboto Condensed';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#05556e;fill-opacity:1;stroke:none"
1423
       xml:space="preserve"><tspan
1424
         id="tspan1192"
1425
         sodipodi:role="line"
1426
         x="588.43488"
1427
         y="355.56967">dependent</tspan></text>
1428
  </g>
1429
</svg>
11430
A images/architecture/logos/html5.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
2
	<title>HTML5 Logo</title>
3
	<path d="M108.4 0h23v22.8h21.2V0h23v69h-23V46h-21v23h-23.2M206 23h-20.3V0h63.7v23H229v46h-23M259.5 0h24.1l14.8 24.3L313.2 0h24.1v69h-23V34.8l-16.1 24.8l-16.1-24.8v34.2h-22.6M348.7 0h23v46.2h32.6V69h-55.6"/>
4
	<path fill="#e44d26" d="M107.6 471l-33-370.4h362.8l-33 370.2L255.7 512"/>
5
	<path fill="#f16529" d="M256 480.5V131H404.3L376 447"/>
6
	<path fill="#ebebeb" d="M142 176.3h114v45.4h-64.2l4.2 46.5h60v45.3H154.4M156.4 336.3H202l3.2 36.3 50.8 13.6v47.4l-93.2-26"/>
7
	<path fill="#fff" d="M369.6 176.3H255.8v45.4h109.6M361.3 268.2H255.8v45.4h56l-5.3 59-50.7 13.6v47.2l93-25.8"/>
8
</svg>
A images/architecture/logos/links.svg
1
<?xml version="1.0" standalone="no"?>
2
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
3
 "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
4
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
5
 width="1280.000000pt" height="1123.000000pt" viewBox="0 0 1280.000000 1123.000000"
6
 preserveAspectRatio="xMidYMid meet">
7
<metadata>
8
Created by potrace 1.15, written by Peter Selinger 2001-2017
9
</metadata>
10
<g transform="translate(0.000000,1123.000000) scale(0.100000,-0.100000)"
11
fill="#000000" stroke="none">
12
<path d="M10280 11220 c-519 -65 -1029 -329 -1455 -755 -330 -329 -633 -774
13
-924 -1355 -236 -472 -422 -961 -534 -1398 l-32 -127 -127 -65 c-178 -91 -334
14
-187 -511 -313 -607 -434 -1133 -956 -1571 -1559 -43 -60 -84 -108 -92 -108
15
-7 0 -62 8 -121 17 -136 22 -589 25 -758 5 -699 -85 -1336 -290 -2102 -678
16
-553 -280 -909 -529 -1258 -879 -438 -438 -700 -924 -776 -1440 -18 -115 -18
17
-441 0 -560 94 -654 471 -1279 971 -1610 580 -384 1363 -489 2260 -304 667
18
139 1326 395 1984 773 639 367 1082 770 1394 1266 50 80 66 97 98 109 515 186
19
874 378 1282 686 372 280 632 526 1123 1063 126 138 148 169 155 214 6 32 10
20
37 28 33 95 -19 406 -22 543 -5 384 47 747 203 1108 475 482 364 932 972 1250
21
1690 478 1080 659 1990 555 2797 -26 207 -52 322 -110 500 -79 239 -200 471
22
-348 668 -80 105 -290 314 -395 392 -293 217 -725 393 -1112 453 -122 19 -423
23
27 -525 15z m450 -1440 c358 -86 592 -377 650 -810 15 -108 15 -387 0 -505
24
-59 -484 -233 -1050 -481 -1563 -187 -388 -330 -615 -532 -844 -60 -68 -255
25
-243 -263 -235 -1 1 13 63 32 137 46 180 63 289 71 445 13 291 -50 570 -181
26
793 -221 378 -584 628 -1008 697 -21 3 -38 9 -38 14 0 4 36 99 79 212 261 677
27
491 1074 792 1364 193 187 314 259 504 300 95 21 275 18 375 -5z m-1904 -3303
28
c96 -100 104 -141 85 -416 l-6 -95 -26 40 c-42 63 -75 133 -97 206 -12 37 -28
29
81 -37 98 -13 24 -33 168 -35 248 0 22 55 -17 116 -81z m-1357 -555 c71 -421
30
218 -746 451 -995 l83 -88 -25 -27 c-13 -15 -85 -94 -159 -177 -286 -320 -519
31
-549 -746 -731 -77 -63 -144 -114 -147 -114 -3 0 -8 17 -12 38 -14 74 -86 270
32
-148 397 -100 207 -211 370 -384 563 l-84 93 76 93 c220 271 538 602 816 852
33
158 141 254 224 257 221 1 -1 11 -58 22 -125z m-3042 -1799 c-3 -21 -15 -100
34
-27 -177 -41 -271 -43 -658 -5 -881 35 -203 106 -390 199 -519 51 -70 161
35
-180 225 -221 l43 -29 -28 -23 c-111 -90 -468 -291 -770 -432 -444 -208 -804
36
-325 -1219 -398 -143 -25 -184 -28 -385 -28 -182 0 -241 4 -308 18 -280 62
37
-466 179 -589 370 -98 152 -133 273 -134 449 0 288 105 494 400 788 329 327
38
725 562 1422 843 371 150 774 253 1059 270 137 8 123 12 117 -30z m1130 -650
39
c-3 -10 -5 -2 -5 17 0 19 2 27 5 18 2 -10 2 -26 0 -35z"/>
40
</g>
41
</svg>
142
A images/architecture/logos/markdown.svg
1
1
<svg xmlns="http://www.w3.org/2000/svg" width="208" height="128" viewBox="0 0 208 128"><rect width="198" height="118" x="5" y="5" ry="10" stroke="#000" stroke-width="10" fill="none"/><path d="M30 98V30h20l20 25 20-25h20v68H90V59L70 84 50 59v39zm125 0l-30-33h20V30h20v35h20z"/></svg>
A images/black-text.png
Binary file
A images/blocked-text.png
Binary file
A images/broken-camera.svg
1
<svg height='19pt' viewBox='0 0 25 19' width='25pt' xmlns='http://www.w3.org/2000/svg'><g fill='#454545'><path d='m8.042969 11.085938c.332031 1.445312 1.660156 2.503906 3.214843 2.558593zm0 0'/><path d='m6.792969 9.621094-.300781.226562.242187.195313c.015625-.144531.03125-.28125.058594-.421875zm0 0'/><path d='m10.597656.949219-2.511718.207031c-.777344.066406-1.429688.582031-1.636719 1.292969l-.367188 1.253906-3.414062.28125c-1.027344.085937-1.792969.949219-1.699219 1.925781l.976562 10.621094c.089844.976562.996094 1.699219 2.023438 1.613281l11.710938-.972656-3.117188-2.484375c-.246094.0625-.5.109375-.765625.132812-2.566406.210938-4.835937-1.597656-5.0625-4.039062-.023437-.25-.019531-.496094 0-.738281l-.242187-.195313.300781-.226562c.359375-1.929688 2.039062-3.472656 4.191406-3.652344.207031-.015625.414063-.015625.617187-.007812l.933594-.707032zm0 0'/><path d='m10.234375 11.070312 2.964844 2.820313c.144531.015625.285156.027344.433593.027344 1.890626 0 3.429688-1.460938 3.429688-3.257813 0-1.792968-1.539062-3.257812-3.429688-3.257812-1.890624 0-3.429687 1.464844-3.429687 3.257812 0 .140625.011719.277344.03125.410156zm0 0'/><path d='m14.488281.808594 1.117188 4.554687-1.042969.546875c2.25.476563 3.84375 2.472656 3.636719 4.714844-.199219 2.191406-2.050781 3.871094-4.285157 4.039062l2.609376 2.957032 4.4375.371094c1.03125.085937 1.9375-.640626 2.027343-1.617188l.976563-10.617188c.089844-.980468-.667969-1.839843-1.699219-1.925781l-3.414063-.285156-.371093-1.253906c-.207031-.710938-.859375-1.226563-1.636719-1.289063zm0 0'/></g></svg>
12
A images/logo64.png
Binary file
A images/resolved-text.png
Binary file
A images/screenshot.png
Binary file
A installer
1
#!/usr/bin/env bash
2
3
# ---------------------------------------------------------------------------
4
# This script cross-compiles application launchers for different platforms.
5
#
6
# The application binaries are self-contained launchers that do not need
7
# to be installed.
8
# ---------------------------------------------------------------------------
9
10
source $HOME/bin/build-template
11
12
readonly APP_NAME=$(find "${SCRIPT_DIR}/src" -type f -name "settings.properties" -exec cat {} \; | grep "application.title=" | cut -d'=' -f2)
13
readonly FILE_APP_JAR="${APP_NAME}.jar"
14
15
ARG_JRE_OS="linux"
16
ARG_JRE_ARCH="amd64"
17
ARG_JRE_VERSION="14.0.1"
18
ARG_JRE_UPDATE="8"
19
ARG_JRE_DIR="jre"
20
21
ARG_DIR_DIST="dist"
22
23
FILE_DIST_EXEC="run.sh"
24
25
ARG_PATH_DIST_JAR="${SCRIPT_DIR}/build/libs/${FILE_APP_JAR}"
26
27
DEPENDENCIES=(
28
  "gradle,https://gradle.org"
29
  "warp-packer,https://github.com/dgiagio/warp"
30
  "tar,https://www.gnu.org/software/tar"
31
  "unzip,http://infozip.sourceforge.net"
32
)
33
34
ARGUMENTS+=(
35
  "a,arch,Target operating system architecture (amd64)"
36
  "b,build,Suppress building application"
37
  "o,os,Target operating system (linux, windows, mac)"
38
  "u,update,Java update version number (${ARG_JRE_UPDATE})"
39
  "v,version,Full Java version (${ARG_JRE_VERSION})"
40
)
41
42
ARCHIVE_EXT="tar.gz"
43
ARCHIVE_APP="tar xf"
44
APP_EXTENSION="bin"
45
46
# ---------------------------------------------------------------------------
47
# Generates
48
# ---------------------------------------------------------------------------
49
execute() {
50
  $do_configure_target
51
  $do_build
52
  $do_clean
53
54
  pushd "${ARG_DIR_DIST}" > /dev/null 2>&1
55
56
  $do_extract_jre
57
  $do_create_launch_script
58
  $do_copy_archive
59
60
  popd > /dev/null 2>&1
61
62
  $do_create_launcher
63
64
  return 1
65
}
66
67
# ---------------------------------------------------------------------------
68
# Configure platform-specific commands and file names.
69
# ---------------------------------------------------------------------------
70
utile_configure_target() {
71
  if [ "${ARG_JRE_OS}" = "windows" ]; then
72
    ARCHIVE_EXT="zip"
73
    ARCHIVE_APP="unzip -qq"
74
    FILE_DIST_EXEC="run.bat"
75
    APP_EXTENSION="exe"
76
    do_create_launch_script=utile_create_launch_script_windows
77
  fi
78
}
79
80
# ---------------------------------------------------------------------------
81
# Build platform-specific überjar.
82
# ---------------------------------------------------------------------------
83
utile_build() {
84
  $log "Build application for ${ARG_JRE_OS}"
85
  gradle clean jar -PtargetOs="${ARG_JRE_OS}"
86
}
87
88
# ---------------------------------------------------------------------------
89
# Purges the existing distribution directory to recreate the launcher.
90
# This refreshes the JRE from the downloaded archive.
91
# ---------------------------------------------------------------------------
92
utile_clean() {
93
  $log "Recreate ${ARG_DIR_DIST}"
94
  rm -rf "${ARG_DIR_DIST}"
95
  mkdir -p "${ARG_DIR_DIST}"
96
}
97
98
# ---------------------------------------------------------------------------
99
# Extract platform-specific Java Runtime Environment. This will download
100
# and cache the required Java Runtime Environment for the target platform.
101
# On subsequent runs, the cached version is used, instead of issuing another
102
# download.
103
# ---------------------------------------------------------------------------
104
utile_extract_jre() {
105
  $log "Extract JRE"
106
  local -r jre_version="${ARG_JRE_VERSION}+${ARG_JRE_UPDATE}"
107
  local -r url_jdk="https://download.bell-sw.com/java/${jre_version}/bellsoft-jre${jre_version}-${ARG_JRE_OS}-${ARG_JRE_ARCH}-full.${ARCHIVE_EXT}"
108
109
  local -r file_jdk="jre-${jre_version}-${ARG_JRE_OS}-${ARG_JRE_ARCH}.${ARCHIVE_EXT}"
110
  local -r path_jdk="/tmp/${file_jdk}"
111
112
  # File must have contents.
113
  if [ ! -s ${path_jdk} ]; then
114
    $log "Download ${url_jdk} to ${path_jdk}"
115
    wget -q "${url_jdk}" -O "${path_jdk}"
116
  fi
117
118
  $log "Unpack ${path_jdk}"
119
  $ARCHIVE_APP "${path_jdk}"
120
121
  local -r dir_jdk="jre-${ARG_JRE_VERSION}-full"
122
123
  $log "Rename ${dir_jdk}-jre to ${ARG_JRE_DIR}"
124
  mv "${dir_jdk}" "${ARG_JRE_DIR}"
125
}
126
127
# ---------------------------------------------------------------------------
128
# Create Linux-specific launch script.
129
# ---------------------------------------------------------------------------
130
utile_create_launch_script_linux() {
131
  $log "Create Linux launch script"
132
133
  cat > "${FILE_DIST_EXEC}" << __EOT
134
#!/usr/bin/env bash
135
136
readonly SCRIPT_SRC="\$(dirname "\${BASH_SOURCE[\${#BASH_SOURCE[@]} - 1]}")"
137
readonly SCRIPT_DIR="\$(cd "\${SCRIPT_SRC}" >/dev/null 2>&1 && pwd)"
138
139
"\${SCRIPT_DIR}/${ARG_JRE_DIR}/bin/java" -jar "\${SCRIPT_DIR}/${FILE_APP_JAR}" "\$@"
140
__EOT
141
142
  chmod +x "${FILE_DIST_EXEC}"
143
}
144
145
# ---------------------------------------------------------------------------
146
# Create Windows-specific launch script.
147
# ---------------------------------------------------------------------------
148
utile_create_launch_script_windows() {
149
  $log "Create Windows launch script"
150
151
  cat > "${FILE_DIST_EXEC}" << __EOT
152
@echo off
153
154
set SCRIPT_DIR=%~dp0
155
"%SCRIPT_DIR%jre\\bin\\java" -jar "%SCRIPT_DIR%\\scrivenvar.jar" %*
156
__EOT
157
158
  # Convert Unix end of line characters (\n) to Windows format (\r\n).
159
  # This avoids any potential line conversion issues with the repository.
160
  sed -i 's/$/\r/' "${FILE_DIST_EXEC}"
161
}
162
163
# ---------------------------------------------------------------------------
164
# Copy application überjar.
165
# ---------------------------------------------------------------------------
166
utile_copy_archive() {
167
  $log "Create copy of ${FILE_APP_JAR}"
168
  cp "${ARG_PATH_DIST_JAR}" "${FILE_APP_JAR}"
169
}
170
171
# ---------------------------------------------------------------------------
172
# Create platform-specific launcher binary.
173
# ---------------------------------------------------------------------------
174
utile_create_launcher() {
175
  $log "Create ${APP_NAME}.${APP_EXTENSION}"
176
177
  # Download uses amd64, but warp-packer differs.
178
  if [ "${ARG_JRE_ARCH}" = "amd64" ]; then
179
    ARG_JRE_ARCH="x64"
180
  fi
181
182
  warp-packer \
183
    --arch "${ARG_JRE_OS}-${ARG_JRE_ARCH}" \
184
    --input_dir "${ARG_DIR_DIST}" \
185
    --exec "${FILE_DIST_EXEC}" \
186
    --output "${APP_NAME}.${APP_EXTENSION}" > /dev/null
187
188
  chmod +x "${APP_NAME}.${APP_EXTENSION}"
189
}
190
191
argument() {
192
  local consume=2
193
194
  case "$1" in
195
    -a|--arch)
196
    ARG_JRE_ARCH="$2"
197
    ;;
198
    -b|--build)
199
    do_build=noop
200
    consume=1
201
    ;;
202
    -o|--os)
203
    ARG_JRE_OS="$2"
204
    ;;
205
    -u|--update)
206
    ARG_JRE_UPDATE="$2"
207
    ;;
208
    -v|--version)
209
    ARG_JRE_VERSION="$2"
210
    ;;
211
  esac
212
213
  return ${consume}
214
}
215
216
do_configure_target=utile_configure_target
217
do_build=utile_build
218
do_clean=utile_clean
219
do_extract_jre=utile_extract_jre
220
do_create_launch_script=utile_create_launch_script_linux
221
do_copy_archive=utile_copy_archive
222
do_create_launcher=utile_create_launcher
223
224
main "$@"
225
1226
A libs/jsymspell/jsymspell-core-1.0-SNAPSHOT-javadoc.jar
Binary file
A libs/jsymspell/jsymspell-core-1.0-SNAPSHOT-sources.jar
Binary file
A libs/jsymspell/jsymspell-core-1.0-SNAPSHOT.jar
Binary file
A licenses/FILE-PREFERENCES.md
1
Released into the Public Domain by David Croft.
2
3
http://www.davidc.net/programming/java/java-preferences-using-file-backing-store
4
http://creativecommons.org/publicdomain/zero/1.0/
5
6
CC0 1.0 Universal (CC0 1.0)
7
8
Public Domain Dedication
9
10
This is a human-readable summary of the Legal Code (read the full text).
11
12
Disclaimer
13
14
No Copyright
15
16
* The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.
17
18
* You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission. See Other Information below.
19
20
This license is acceptable for Free Cultural Works.
21
22
Other Information
23
24
* In no way are the patent or trademark rights of any person affected by CC0, nor are the rights that other persons may have in the work or in how the work is used, such as publicity or privacy rights.
25
* Unless expressly stated otherwise, the person who associated a work with this deed makes no warranties about the work, and disclaims liability for all uses of the work, to the fullest extent permitted by applicable law.
26
* When using or citing the work, you should not imply endorsement by the author or the affirmer.
27
128
A licenses/FLEXMARK.md
1
Copyright (c) 2015-2016, Atlassian Pty Ltd
2
All rights reserved.
3
4
Copyright (c) 2016-2018, Vladimir Schneider,
5
All rights reserved.
6
7
Redistribution and use in source and binary forms, with or without
8
modification, are permitted provided that the following conditions are met:
9
10
* Redistributions of source code must retain the above copyright notice, this
11
  list of conditions and the following disclaimer.
12
13
* Redistributions in binary form must reproduce the above copyright notice,
14
  this list of conditions and the following disclaimer in the documentation
15
  and/or other materials provided with the distribution.
16
17
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
21
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25
OR TORT (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.
127
A licenses/FLOWLESS.md
1
Copyright (c) 2014, TomasMikula
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 notice, this
8
  list of conditions and the following disclaimer.
9
10
* Redistributions in binary form must reproduce the above copyright notice,
11
  this list of conditions and the following disclaimer in the documentation
12
  and/or other materials provided with the distribution.
13
14
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
124
A licenses/FONT-AWESOME-FX.txt
11
2
                                 Apache License
3
                           Version 2.0, January 2004
4
                        http://www.apache.org/licenses/
5
6
   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
8
   1. Definitions.
9
10
      "License" shall mean the terms and conditions for use, reproduction,
11
      and distribution as defined by Sections 1 through 9 of this document.
12
13
      "Licensor" shall mean the copyright owner or entity authorized by
14
      the copyright owner that is granting the License.
15
16
      "Legal Entity" shall mean the union of the acting entity and all
17
      other entities that control, are controlled by, or are under common
18
      control with that entity. For the purposes of this definition,
19
      "control" means (i) the power, direct or indirect, to cause the
20
      direction or management of such entity, whether by contract or
21
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
      outstanding shares, or (iii) beneficial ownership of such entity.
23
24
      "You" (or "Your") shall mean an individual or Legal Entity
25
      exercising permissions granted by this License.
26
27
      "Source" form shall mean the preferred form for making modifications,
28
      including but not limited to software source code, documentation
29
      source, and configuration files.
30
31
      "Object" form shall mean any form resulting from mechanical
32
      transformation or translation of a Source form, including but
33
      not limited to compiled object code, generated documentation,
34
      and conversions to other media types.
35
36
      "Work" shall mean the work of authorship, whether in Source or
37
      Object form, made available under the License, as indicated by a
38
      copyright notice that is included in or attached to the work
39
      (an example is provided in the Appendix below).
40
41
      "Derivative Works" shall mean any work, whether in Source or Object
42
      form, that is based on (or derived from) the Work and for which the
43
      editorial revisions, annotations, elaborations, or other modifications
44
      represent, as a whole, an original work of authorship. For the purposes
45
      of this License, Derivative Works shall not include works that remain
46
      separable from, or merely link (or bind by name) to the interfaces of,
47
      the Work and Derivative Works thereof.
48
49
      "Contribution" shall mean any work of authorship, including
50
      the original version of the Work and any modifications or additions
51
      to that Work or Derivative Works thereof, that is intentionally
52
      submitted to Licensor for inclusion in the Work by the copyright owner
53
      or by an individual or Legal Entity authorized to submit on behalf of
54
      the copyright owner. For the purposes of this definition, "submitted"
55
      means any form of electronic, verbal, or written communication sent
56
      to the Licensor or its representatives, including but not limited to
57
      communication on electronic mailing lists, source code control systems,
58
      and issue tracking systems that are managed by, or on behalf of, the
59
      Licensor for the purpose of discussing and improving the Work, but
60
      excluding communication that is conspicuously marked or otherwise
61
      designated in writing by the copyright owner as "Not a Contribution."
62
63
      "Contributor" shall mean Licensor and any individual or Legal Entity
64
      on behalf of whom a Contribution has been received by Licensor and
65
      subsequently incorporated within the Work.
66
67
   2. Grant of Copyright License. Subject to the terms and conditions of
68
      this License, each Contributor hereby grants to You a perpetual,
69
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
      copyright license to reproduce, prepare Derivative Works of,
71
      publicly display, publicly perform, sublicense, and distribute the
72
      Work and such Derivative Works in Source or Object form.
73
74
   3. Grant of Patent License. Subject to the terms and conditions of
75
      this License, each Contributor hereby grants to You a perpetual,
76
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
      (except as stated in this section) patent license to make, have made,
78
      use, offer to sell, sell, import, and otherwise transfer the Work,
79
      where such license applies only to those patent claims licensable
80
      by such Contributor that are necessarily infringed by their
81
      Contribution(s) alone or by combination of their Contribution(s)
82
      with the Work to which such Contribution(s) was submitted. If You
83
      institute patent litigation against any entity (including a
84
      cross-claim or counterclaim in a lawsuit) alleging that the Work
85
      or a Contribution incorporated within the Work constitutes direct
86
      or contributory patent infringement, then any patent licenses
87
      granted to You under this License for that Work shall terminate
88
      as of the date such litigation is filed.
89
90
   4. Redistribution. You may reproduce and distribute copies of the
91
      Work or Derivative Works thereof in any medium, with or without
92
      modifications, and in Source or Object form, provided that You
93
      meet the following conditions:
94
95
      (a) You must give any other recipients of the Work or
96
          Derivative Works a copy of this License; and
97
98
      (b) You must cause any modified files to carry prominent notices
99
          stating that You changed the files; and
100
101
      (c) You must retain, in the Source form of any Derivative Works
102
          that You distribute, all copyright, patent, trademark, and
103
          attribution notices from the Source form of the Work,
104
          excluding those notices that do not pertain to any part of
105
          the Derivative Works; and
106
107
      (d) If the Work includes a "NOTICE" text file as part of its
108
          distribution, then any Derivative Works that You distribute must
109
          include a readable copy of the attribution notices contained
110
          within such NOTICE file, excluding those notices that do not
111
          pertain to any part of the Derivative Works, in at least one
112
          of the following places: within a NOTICE text file distributed
113
          as part of the Derivative Works; within the Source form or
114
          documentation, if provided along with the Derivative Works; or,
115
          within a display generated by the Derivative Works, if and
116
          wherever such third-party notices normally appear. The contents
117
          of the NOTICE file are for informational purposes only and
118
          do not modify the License. You may add Your own attribution
119
          notices within Derivative Works that You distribute, alongside
120
          or as an addendum to the NOTICE text from the Work, provided
121
          that such additional attribution notices cannot be construed
122
          as modifying the License.
123
124
      You may add Your own copyright statement to Your modifications and
125
      may provide additional or different license terms and conditions
126
      for use, reproduction, or distribution of Your modifications, or
127
      for any such Derivative Works as a whole, provided Your use,
128
      reproduction, and distribution of the Work otherwise complies with
129
      the conditions stated in this License.
130
131
   5. Submission of Contributions. Unless You explicitly state otherwise,
132
      any Contribution intentionally submitted for inclusion in the Work
133
      by You to the Licensor shall be under the terms and conditions of
134
      this License, without any additional terms or conditions.
135
      Notwithstanding the above, nothing herein shall supersede or modify
136
      the terms of any separate license agreement you may have executed
137
      with Licensor regarding such Contributions.
138
139
   6. Trademarks. This License does not grant permission to use the trade
140
      names, trademarks, service marks, or product names of the Licensor,
141
      except as required for reasonable and customary use in describing the
142
      origin of the Work and reproducing the content of the NOTICE file.
143
144
   7. Disclaimer of Warranty. Unless required by applicable law or
145
      agreed to in writing, Licensor provides the Work (and each
146
      Contributor provides its Contributions) on an "AS IS" BASIS,
147
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
      implied, including, without limitation, any warranties or conditions
149
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
      PARTICULAR PURPOSE. You are solely responsible for determining the
151
      appropriateness of using or redistributing the Work and assume any
152
      risks associated with Your exercise of permissions under this License.
153
154
   8. Limitation of Liability. In no event and under no legal theory,
155
      whether in tort (including negligence), contract, or otherwise,
156
      unless required by applicable law (such as deliberate and grossly
157
      negligent acts) or agreed to in writing, shall any Contributor be
158
      liable to You for damages, including any direct, indirect, special,
159
      incidental, or consequential damages of any character arising as a
160
      result of this License or out of the use or inability to use the
161
      Work (including but not limited to damages for loss of goodwill,
162
      work stoppage, computer failure or malfunction, or any and all
163
      other commercial damages or losses), even if such Contributor
164
      has been advised of the possibility of such damages.
165
166
   9. Accepting Warranty or Additional Liability. While redistributing
167
      the Work or Derivative Works thereof, You may choose to offer,
168
      and charge a fee for, acceptance of support, warranty, indemnity,
169
      or other liability obligations and/or rights consistent with this
170
      License. However, in accepting such obligations, You may act only
171
      on Your own behalf and on Your sole responsibility, not on behalf
172
      of any other Contributor, and only if You agree to indemnify,
173
      defend, and hold each Contributor harmless for any liability
174
      incurred by, or claims asserted against, such Contributor by reason
175
      of your accepting any such warranty or additional liability.
176
177
   END OF TERMS AND CONDITIONS
178
179
   APPENDIX: How to apply the Apache License to your work.
180
181
      To apply the Apache License to your work, attach the following
182
      boilerplate notice, with the fields enclosed by brackets "[]"
183
      replaced with your own identifying information. (Don't include
184
      the brackets!)  The text should be enclosed in the appropriate
185
      comment syntax for the file format. We also recommend that a
186
      file or class name and description of purpose be included on the
187
      same "printed page" as the copyright notice for easier
188
      identification within third-party archives.
189
190
   Copyright [yyyy] [name of copyright owner]
191
192
   Licensed under the Apache License, Version 2.0 (the "License");
193
   you may not use this file except in compliance with the License.
194
   You may obtain a copy of the License at
195
196
       http://www.apache.org/licenses/LICENSE-2.0
197
198
   Unless required by applicable law or agreed to in writing, software
199
   distributed under the License is distributed on an "AS IS" BASIS,
200
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
   See the License for the specific language governing permissions and
202
   limitations under the License.
203
A licenses/JSYMSPELL.md
1
MIT License
2
3
Copyright (c) 2019 Raul Garcia
4
5
Permission is hereby granted, free of charge, to any person obtaining a copy
6
of this software and associated documentation files (the "Software"), to deal
7
in the Software without restriction, including without limitation the rights
8
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
copies of the Software, and to permit persons to whom the Software is
10
furnished to do so, subject to the following conditions:
11
12
The above copyright notice and this permission notice shall be included in all
13
copies or substantial portions of the Software.
14
15
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
SOFTWARE.
122
A licenses/JUNIVERSAL-CHARDET.md
1
Version: MPL 1.1/GPL 2.0/LGPL 2.1
2
3
The contents of this file are subject to the Mozilla Public License Version
4
1.1 (the "License"); you may not use this file except in compliance with
5
the License. You may obtain a copy of the License at
6
http://www.mozilla.org/MPL/
7
8
Software distributed under the License is distributed on an "AS IS" basis,
9
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
10
for the specific language governing rights and limitations under the
11
License.
12
13
The Original Code is Mozilla Universal charset detector code.
14
15
The Initial Developer of the Original Code is
16
Netscape Communications Corporation.
17
Portions created by the Initial Developer are Copyright (C) 2001
18
the Initial Developer. All Rights Reserved.
19
20
Contributor(s):
21
        Shy Shalom <shooshX@gmail.com>
22
        Kohei TAKETA <k-tak@void.in> (Java port)
23
24
Alternatively, the contents of this file may be used under the terms of
25
either the GNU General Public License Version 2 or later (the "GPL"), or
26
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27
in which case the provisions of the GPL or the LGPL are applicable instead
28
of those above. If you wish to allow use of your version of this file only
29
under the terms of either the GPL or the LGPL, and not to allow others to
30
use your version of this file under the terms of the MPL, indicate your
31
decision by deleting the provisions above and replace them with the notice
32
and other provisions required by the GPL or the LGPL. If you do not delete
33
the provisions above, a recipient may use your version of this file under
34
the terms of any one of the MPL, the GPL or the LGPL.
35
136
A licenses/MARKDOWN-WRITER-FX.md
1
Copyright (c) 2015 Karl Tauber <karl@jformdesigner.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 licenses/MIG-LAYOUT.md
1
Copyright (c) 2000 Mikael Grev
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
6
are met:
7
1. Redistributions of source code must retain the above copyright
8
   notice, this list of conditions and the following disclaimer.
9
2. Redistributions in binary form must reproduce the above copyright
10
   notice, this list of conditions and the following disclaimer in the
11
   documentation and/or other materials provided with the distribution.
12
3. The name of the author may not be used to endorse or promote products
13
   derived from this software without specific prior written permission.
14
15
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18
IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20
NOT 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 OF
24
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
126
A licenses/PREFERENCES-FX.txt
1
                                 Apache License
2
                           Version 2.0, January 2004
3
                        http://www.apache.org/licenses/
4
5
   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
7
   1. Definitions.
8
9
      "License" shall mean the terms and conditions for use, reproduction,
10
      and distribution as defined by Sections 1 through 9 of this document.
11
12
      "Licensor" shall mean the copyright owner or entity authorized by
13
      the copyright owner that is granting the License.
14
15
      "Legal Entity" shall mean the union of the acting entity and all
16
      other entities that control, are controlled by, or are under common
17
      control with that entity. For the purposes of this definition,
18
      "control" means (i) the power, direct or indirect, to cause the
19
      direction or management of such entity, whether by contract or
20
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
      outstanding shares, or (iii) beneficial ownership of such entity.
22
23
      "You" (or "Your") shall mean an individual or Legal Entity
24
      exercising permissions granted by this License.
25
26
      "Source" form shall mean the preferred form for making modifications,
27
      including but not limited to software source code, documentation
28
      source, and configuration files.
29
30
      "Object" form shall mean any form resulting from mechanical
31
      transformation or translation of a Source form, including but
32
      not limited to compiled object code, generated documentation,
33
      and conversions to other media types.
34
35
      "Work" shall mean the work of authorship, whether in Source or
36
      Object form, made available under the License, as indicated by a
37
      copyright notice that is included in or attached to the work
38
      (an example is provided in the Appendix below).
39
40
      "Derivative Works" shall mean any work, whether in Source or Object
41
      form, that is based on (or derived from) the Work and for which the
42
      editorial revisions, annotations, elaborations, or other modifications
43
      represent, as a whole, an original work of authorship. For the purposes
44
      of this License, Derivative Works shall not include works that remain
45
      separable from, or merely link (or bind by name) to the interfaces of,
46
      the Work and Derivative Works thereof.
47
48
      "Contribution" shall mean any work of authorship, including
49
      the original version of the Work and any modifications or additions
50
      to that Work or Derivative Works thereof, that is intentionally
51
      submitted to Licensor for inclusion in the Work by the copyright owner
52
      or by an individual or Legal Entity authorized to submit on behalf of
53
      the copyright owner. For the purposes of this definition, "submitted"
54
      means any form of electronic, verbal, or written communication sent
55
      to the Licensor or its representatives, including but not limited to
56
      communication on electronic mailing lists, source code control systems,
57
      and issue tracking systems that are managed by, or on behalf of, the
58
      Licensor for the purpose of discussing and improving the Work, but
59
      excluding communication that is conspicuously marked or otherwise
60
      designated in writing by the copyright owner as "Not a Contribution."
61
62
      "Contributor" shall mean Licensor and any individual or Legal Entity
63
      on behalf of whom a Contribution has been received by Licensor and
64
      subsequently incorporated within the Work.
65
66
   2. Grant of Copyright License. Subject to the terms and conditions of
67
      this License, each Contributor hereby grants to You a perpetual,
68
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
      copyright license to reproduce, prepare Derivative Works of,
70
      publicly display, publicly perform, sublicense, and distribute the
71
      Work and such Derivative Works in Source or Object form.
72
73
   3. Grant of Patent License. Subject to the terms and conditions of
74
      this License, each Contributor hereby grants to You a perpetual,
75
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
      (except as stated in this section) patent license to make, have made,
77
      use, offer to sell, sell, import, and otherwise transfer the Work,
78
      where such license applies only to those patent claims licensable
79
      by such Contributor that are necessarily infringed by their
80
      Contribution(s) alone or by combination of their Contribution(s)
81
      with the Work to which such Contribution(s) was submitted. If You
82
      institute patent litigation against any entity (including a
83
      cross-claim or counterclaim in a lawsuit) alleging that the Work
84
      or a Contribution incorporated within the Work constitutes direct
85
      or contributory patent infringement, then any patent licenses
86
      granted to You under this License for that Work shall terminate
87
      as of the date such litigation is filed.
88
89
   4. Redistribution. You may reproduce and distribute copies of the
90
      Work or Derivative Works thereof in any medium, with or without
91
      modifications, and in Source or Object form, provided that You
92
      meet the following conditions:
93
94
      (a) You must give any other recipients of the Work or
95
          Derivative Works a copy of this License; and
96
97
      (b) You must cause any modified files to carry prominent notices
98
          stating that You changed the files; and
99
100
      (c) You must retain, in the Source form of any Derivative Works
101
          that You distribute, all copyright, patent, trademark, and
102
          attribution notices from the Source form of the Work,
103
          excluding those notices that do not pertain to any part of
104
          the Derivative Works; and
105
106
      (d) If the Work includes a "NOTICE" text file as part of its
107
          distribution, then any Derivative Works that You distribute must
108
          include a readable copy of the attribution notices contained
109
          within such NOTICE file, excluding those notices that do not
110
          pertain to any part of the Derivative Works, in at least one
111
          of the following places: within a NOTICE text file distributed
112
          as part of the Derivative Works; within the Source form or
113
          documentation, if provided along with the Derivative Works; or,
114
          within a display generated by the Derivative Works, if and
115
          wherever such third-party notices normally appear. The contents
116
          of the NOTICE file are for informational purposes only and
117
          do not modify the License. You may add Your own attribution
118
          notices within Derivative Works that You distribute, alongside
119
          or as an addendum to the NOTICE text from the Work, provided
120
          that such additional attribution notices cannot be construed
121
          as modifying the License.
122
123
      You may add Your own copyright statement to Your modifications and
124
      may provide additional or different license terms and conditions
125
      for use, reproduction, or distribution of Your modifications, or
126
      for any such Derivative Works as a whole, provided Your use,
127
      reproduction, and distribution of the Work otherwise complies with
128
      the conditions stated in this License.
129
130
   5. Submission of Contributions. Unless You explicitly state otherwise,
131
      any Contribution intentionally submitted for inclusion in the Work
132
      by You to the Licensor shall be under the terms and conditions of
133
      this License, without any additional terms or conditions.
134
      Notwithstanding the above, nothing herein shall supersede or modify
135
      the terms of any separate license agreement you may have executed
136
      with Licensor regarding such Contributions.
137
138
   6. Trademarks. This License does not grant permission to use the trade
139
      names, trademarks, service marks, or product names of the Licensor,
140
      except as required for reasonable and customary use in describing the
141
      origin of the Work and reproducing the content of the NOTICE file.
142
143
   7. Disclaimer of Warranty. Unless required by applicable law or
144
      agreed to in writing, Licensor provides the Work (and each
145
      Contributor provides its Contributions) on an "AS IS" BASIS,
146
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
      implied, including, without limitation, any warranties or conditions
148
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
      PARTICULAR PURPOSE. You are solely responsible for determining the
150
      appropriateness of using or redistributing the Work and assume any
151
      risks associated with Your exercise of permissions under this License.
152
153
   8. Limitation of Liability. In no event and under no legal theory,
154
      whether in tort (including negligence), contract, or otherwise,
155
      unless required by applicable law (such as deliberate and grossly
156
      negligent acts) or agreed to in writing, shall any Contributor be
157
      liable to You for damages, including any direct, indirect, special,
158
      incidental, or consequential damages of any character arising as a
159
      result of this License or out of the use or inability to use the
160
      Work (including but not limited to damages for loss of goodwill,
161
      work stoppage, computer failure or malfunction, or any and all
162
      other commercial damages or losses), even if such Contributor
163
      has been advised of the possibility of such damages.
164
165
   9. Accepting Warranty or Additional Liability. While redistributing
166
      the Work or Derivative Works thereof, You may choose to offer,
167
      and charge a fee for, acceptance of support, warranty, indemnity,
168
      or other liability obligations and/or rights consistent with this
169
      License. However, in accepting such obligations, You may act only
170
      on Your own behalf and on Your sole responsibility, not on behalf
171
      of any other Contributor, and only if You agree to indemnify,
172
      defend, and hold each Contributor harmless for any liability
173
      incurred by, or claims asserted against, such Contributor by reason
174
      of your accepting any such warranty or additional liability.
175
176
   END OF TERMS AND CONDITIONS
177
178
   APPENDIX: How to apply the Apache License to your work.
179
180
      To apply the Apache License to your work, attach the following
181
      boilerplate notice, with the fields enclosed by brackets "{}"
182
      replaced with your own identifying information. (Don't include
183
      the brackets!)  The text should be enclosed in the appropriate
184
      comment syntax for the file format. We also recommend that a
185
      file or class name and description of purpose be included on the
186
      same "printed page" as the copyright notice for easier
187
      identification within third-party archives.
188
189
   Copyright {yyyy} {name of copyright owner}
190
191
   Licensed under the Apache License, Version 2.0 (the "License");
192
   you may not use this file except in compliance with the License.
193
   You may obtain a copy of the License at
194
195
       http://www.apache.org/licenses/LICENSE-2.0
196
197
   Unless required by applicable law or agreed to in writing, software
198
   distributed under the License is distributed on an "AS IS" BASIS,
199
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
   See the License for the specific language governing permissions and
201
   limitations under the License.
1202
A licenses/REACT-FX.md
1
Copyright (c) 2013-2014, Tomas Mikula
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
6
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
8
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
10
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
111
A licenses/RENJIN.txt
1
		    GNU GENERAL PUBLIC LICENSE
2
		       Version 2, June 1991
3
4
 Copyright (C) 1989, 1991 Free Software Foundation, Inc.
5
                       51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
6
 Everyone is permitted to copy and distribute verbatim copies
7
 of this license document, but changing it is not allowed.
8
9
			    Preamble
10
11
  The licenses for most software are designed to take away your
12
freedom to share and change it.  By contrast, the GNU General Public
13
License is intended to guarantee your freedom to share and change free
14
software--to make sure the software is free for all its users.  This
15
General Public License applies to most of the Free Software
16
Foundation's software and to any other program whose authors commit to
17
using it.  (Some other Free Software Foundation software is covered by
18
the GNU Library General Public License instead.)  You can apply it to
19
your programs, too.
20
21
  When we speak of free software, we are referring to freedom, not
22
price.  Our General Public Licenses are designed to make sure that you
23
have the freedom to distribute copies of free software (and charge for
24
this service if you wish), that you receive source code or can get it
25
if you want it, that you can change the software or use pieces of it
26
in new free programs; and that you know you can do these things.
27
28
  To protect your rights, we need to make restrictions that forbid
29
anyone to deny you these rights or to ask you to surrender the rights.
30
These restrictions translate to certain responsibilities for you if you
31
distribute copies of the software, or if you modify it.
32
33
  For example, if you distribute copies of such a program, whether
34
gratis or for a fee, you must give the recipients all the rights that
35
you have.  You must make sure that they, too, receive or can get the
36
source code.  And you must show them these terms so they know their
37
rights.
38
39
  We protect your rights with two steps: (1) copyright the software, and
40
(2) offer you this license which gives you legal permission to copy,
41
distribute and/or modify the software.
42
43
  Also, for each author's protection and ours, we want to make certain
44
that everyone understands that there is no warranty for this free
45
software.  If the software is modified by someone else and passed on, we
46
want its recipients to know that what they have is not the original, so
47
that any problems introduced by others will not reflect on the original
48
authors' reputations.
49
50
  Finally, any free program is threatened constantly by software
51
patents.  We wish to avoid the danger that redistributors of a free
52
program will individually obtain patent licenses, in effect making the
53
program proprietary.  To prevent this, we have made it clear that any
54
patent must be licensed for everyone's free use or not licensed at all.
55
56
  The precise terms and conditions for copying, distribution and
57
modification follow.
58

59
		    GNU GENERAL PUBLIC LICENSE
60
   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61
62
  0. This License applies to any program or other work which contains
63
a notice placed by the copyright holder saying it may be distributed
64
under the terms of this General Public License.  The "Program", below,
65
refers to any such program or work, and a "work based on the Program"
66
means either the Program or any derivative work under copyright law:
67
that is to say, a work containing the Program or a portion of it,
68
either verbatim or with modifications and/or translated into another
69
language.  (Hereinafter, translation is included without limitation in
70
the term "modification".)  Each licensee is addressed as "you".
71
72
Activities other than copying, distribution and modification are not
73
covered by this License; they are outside its scope.  The act of
74
running the Program is not restricted, and the output from the Program
75
is covered only if its contents constitute a work based on the
76
Program (independent of having been made by running the Program).
77
Whether that is true depends on what the Program does.
78
79
  1. You may copy and distribute verbatim copies of the Program's
80
source code as you receive it, in any medium, provided that you
81
conspicuously and appropriately publish on each copy an appropriate
82
copyright notice and disclaimer of warranty; keep intact all the
83
notices that refer to this License and to the absence of any warranty;
84
and give any other recipients of the Program a copy of this License
85
along with the Program.
86
87
You may charge a fee for the physical act of transferring a copy, and
88
you may at your option offer warranty protection in exchange for a fee.
89
90
  2. You may modify your copy or copies of the Program or any portion
91
of it, thus forming a work based on the Program, and copy and
92
distribute such modifications or work under the terms of Section 1
93
above, provided that you also meet all of these conditions:
94
95
    a) You must cause the modified files to carry prominent notices
96
    stating that you changed the files and the date of any change.
97
98
    b) You must cause any work that you distribute or publish, that in
99
    whole or in part contains or is derived from the Program or any
100
    part thereof, to be licensed as a whole at no charge to all third
101
    parties under the terms of this License.
102
103
    c) If the modified program normally reads commands interactively
104
    when run, you must cause it, when started running for such
105
    interactive use in the most ordinary way, to print or display an
106
    announcement including an appropriate copyright notice and a
107
    notice that there is no warranty (or else, saying that you provide
108
    a warranty) and that users may redistribute the program under
109
    these conditions, and telling the user how to view a copy of this
110
    License.  (Exception: if the Program itself is interactive but
111
    does not normally print such an announcement, your work based on
112
    the Program is not required to print an announcement.)
113

114
These requirements apply to the modified work as a whole.  If
115
identifiable sections of that work are not derived from the Program,
116
and can be reasonably considered independent and separate works in
117
themselves, then this License, and its terms, do not apply to those
118
sections when you distribute them as separate works.  But when you
119
distribute the same sections as part of a whole which is a work based
120
on the Program, the distribution of the whole must be on the terms of
121
this License, whose permissions for other licensees extend to the
122
entire whole, and thus to each and every part regardless of who wrote it.
123
124
Thus, it is not the intent of this section to claim rights or contest
125
your rights to work written entirely by you; rather, the intent is to
126
exercise the right to control the distribution of derivative or
127
collective works based on the Program.
128
129
In addition, mere aggregation of another work not based on the Program
130
with the Program (or with a work based on the Program) on a volume of
131
a storage or distribution medium does not bring the other work under
132
the scope of this License.
133
134
  3. You may copy and distribute the Program (or a work based on it,
135
under Section 2) in object code or executable form under the terms of
136
Sections 1 and 2 above provided that you also do one of the following:
137
138
    a) Accompany it with the complete corresponding machine-readable
139
    source code, which must be distributed under the terms of Sections
140
    1 and 2 above on a medium customarily used for software interchange; or,
141
142
    b) Accompany it with a written offer, valid for at least three
143
    years, to give any third party, for a charge no more than your
144
    cost of physically performing source distribution, a complete
145
    machine-readable copy of the corresponding source code, to be
146
    distributed under the terms of Sections 1 and 2 above on a medium
147
    customarily used for software interchange; or,
148
149
    c) Accompany it with the information you received as to the offer
150
    to distribute corresponding source code.  (This alternative is
151
    allowed only for noncommercial distribution and only if you
152
    received the program in object code or executable form with such
153
    an offer, in accord with Subsection b above.)
154
155
The source code for a work means the preferred form of the work for
156
making modifications to it.  For an executable work, complete source
157
code means all the source code for all modules it contains, plus any
158
associated interface definition files, plus the scripts used to
159
control compilation and installation of the executable.  However, as a
160
special exception, the source code distributed need not include
161
anything that is normally distributed (in either source or binary
162
form) with the major components (compiler, kernel, and so on) of the
163
operating system on which the executable runs, unless that component
164
itself accompanies the executable.
165
166
If distribution of executable or object code is made by offering
167
access to copy from a designated place, then offering equivalent
168
access to copy the source code from the same place counts as
169
distribution of the source code, even though third parties are not
170
compelled to copy the source along with the object code.
171

172
  4. You may not copy, modify, sublicense, or distribute the Program
173
except as expressly provided under this License.  Any attempt
174
otherwise to copy, modify, sublicense or distribute the Program is
175
void, and will automatically terminate your rights under this License.
176
However, parties who have received copies, or rights, from you under
177
this License will not have their licenses terminated so long as such
178
parties remain in full compliance.
179
180
  5. You are not required to accept this License, since you have not
181
signed it.  However, nothing else grants you permission to modify or
182
distribute the Program or its derivative works.  These actions are
183
prohibited by law if you do not accept this License.  Therefore, by
184
modifying or distributing the Program (or any work based on the
185
Program), you indicate your acceptance of this License to do so, and
186
all its terms and conditions for copying, distributing or modifying
187
the Program or works based on it.
188
189
  6. Each time you redistribute the Program (or any work based on the
190
Program), the recipient automatically receives a license from the
191
original licensor to copy, distribute or modify the Program subject to
192
these terms and conditions.  You may not impose any further
193
restrictions on the recipients' exercise of the rights granted herein.
194
You are not responsible for enforcing compliance by third parties to
195
this License.
196
197
  7. If, as a consequence of a court judgment or allegation of patent
198
infringement or for any other reason (not limited to patent issues),
199
conditions are imposed on you (whether by court order, agreement or
200
otherwise) that contradict the conditions of this License, they do not
201
excuse you from the conditions of this License.  If you cannot
202
distribute so as to satisfy simultaneously your obligations under this
203
License and any other pertinent obligations, then as a consequence you
204
may not distribute the Program at all.  For example, if a patent
205
license would not permit royalty-free redistribution of the Program by
206
all those who receive copies directly or indirectly through you, then
207
the only way you could satisfy both it and this License would be to
208
refrain entirely from distribution of the Program.
209
210
If any portion of this section is held invalid or unenforceable under
211
any particular circumstance, the balance of the section is intended to
212
apply and the section as a whole is intended to apply in other
213
circumstances.
214
215
It is not the purpose of this section to induce you to infringe any
216
patents or other property right claims or to contest validity of any
217
such claims; this section has the sole purpose of protecting the
218
integrity of the free software distribution system, which is
219
implemented by public license practices.  Many people have made
220
generous contributions to the wide range of software distributed
221
through that system in reliance on consistent application of that
222
system; it is up to the author/donor to decide if he or she is willing
223
to distribute software through any other system and a licensee cannot
224
impose that choice.
225
226
This section is intended to make thoroughly clear what is believed to
227
be a consequence of the rest of this License.
228

229
  8. If the distribution and/or use of the Program is restricted in
230
certain countries either by patents or by copyrighted interfaces, the
231
original copyright holder who places the Program under this License
232
may add an explicit geographical distribution limitation excluding
233
those countries, so that distribution is permitted only in or among
234
countries not thus excluded.  In such case, this License incorporates
235
the limitation as if written in the body of this License.
236
237
  9. The Free Software Foundation may publish revised and/or new versions
238
of the General Public License from time to time.  Such new versions will
239
be similar in spirit to the present version, but may differ in detail to
240
address new problems or concerns.
241
242
Each version is given a distinguishing version number.  If the Program
243
specifies a version number of this License which applies to it and "any
244
later version", you have the option of following the terms and conditions
245
either of that version or of any later version published by the Free
246
Software Foundation.  If the Program does not specify a version number of
247
this License, you may choose any version ever published by the Free Software
248
Foundation.
249
250
  10. If you wish to incorporate parts of the Program into other free
251
programs whose distribution conditions are different, write to the author
252
to ask for permission.  For software which is copyrighted by the Free
253
Software Foundation, write to the Free Software Foundation; we sometimes
254
make exceptions for this.  Our decision will be guided by the two goals
255
of preserving the free status of all derivatives of our free software and
256
of promoting the sharing and reuse of software generally.
257
258
			    NO WARRANTY
259
260
  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
262
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
266
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
267
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268
REPAIR OR CORRECTION.
269
270
  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278
POSSIBILITY OF SUCH DAMAGES.
279
280
		     END OF TERMS AND CONDITIONS
281

282
	    How to Apply These Terms to Your New Programs
283
284
  If you develop a new program, and you want it to be of the greatest
285
possible use to the public, the best way to achieve this is to make it
286
free software which everyone can redistribute and change under these terms.
287
288
  To do so, attach the following notices to the program.  It is safest
289
to attach them to the start of each source file to most effectively
290
convey the exclusion of warranty; and each file should have at least
291
the "copyright" line and a pointer to where the full notice is found.
292
293
    <one line to give the program's name and a brief idea of what it does.>
294
    Copyright (C) <year>  <name of author>
295
296
    This program is free software; you can redistribute it and/or modify
297
    it under the terms of the GNU General Public License as published by
298
    the Free Software Foundation; either version 2 of the License, or
299
    (at your option) any later version.
300
301
    This program is distributed in the hope that it will be useful,
302
    but WITHOUT ANY WARRANTY; without even the implied warranty of
303
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
304
    GNU General Public License for more details.
305
306
    You should have received a copy of the GNU General Public License
307
    along with this program; if not, write to the Free Software
308
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
309
310
311
Also add information on how to contact you by electronic and paper mail.
312
313
If the program is interactive, make it output a short notice like this
314
when it starts in an interactive mode:
315
316
    Gnomovision version 69, Copyright (C) year name of author
317
    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
318
    This is free software, and you are welcome to redistribute it
319
    under certain conditions; type `show c' for details.
320
321
The hypothetical commands `show w' and `show c' should show the appropriate
322
parts of the General Public License.  Of course, the commands you use may
323
be called something other than `show w' and `show c'; they could even be
324
mouse-clicks or menu items--whatever suits your program.
325
326
You should also get your employer (if you work as a programmer) or your
327
school, if any, to sign a "copyright disclaimer" for the program, if
328
necessary.  Here is a sample; alter the names:
329
330
  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
331
  `Gnomovision' (which makes passes at compilers) written by James Hacker.
332
333
  <signature of Ty Coon>, 1 April 1989
334
  Ty Coon, President of Vice
335
336
This General Public License does not permit incorporating your program into
337
proprietary programs.  If your program is a subroutine library, you may
338
consider it more useful to permit linking proprietary applications with the
339
library.  If this is what you want to do, use the GNU Library General
340
Public License instead of this License.
1341
A licenses/RICH-TEXT-FX.md
1
Copyright (c) 2013-2017, Tomas Mikula and contributors
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
6
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
8
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
10
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
111
A licenses/SAXON-HE.txt
1
                             Mozilla Public License
2
                                  Version 2.0
3
4
1. Definitions
5
6
   1.1. “Contributor”
7
          means each individual or legal entity that creates, contributes
8
          to the creation of, or owns Covered Software.
9
10
   1.2. “Contributor Version”
11
          means the combination of the Contributions of others (if any)
12
          used by a Contributor and that particular Contributor’s
13
          Contribution.
14
15
   1.3. “Contribution”
16
          means Covered Software of a particular Contributor.
17
18
   1.4. “Covered Software”
19
          means Source Code Form to which the initial Contributor has
20
          attached the notice in Exhibit A, the Executable Form of such
21
          Source Code Form, and Modifications of such Source Code Form, in
22
          each case including portions thereof.
23
24
   1.5. “Incompatible With Secondary Licenses”
25
          means
26
27
         a. that the initial Contributor has attached the notice described
28
            in Exhibit B to the Covered Software; or
29
         b. that the Covered Software was made available under the terms
30
            of version 1.1 or earlier of the License, but not also under
31
            the terms of a Secondary License.
32
33
   1.6. “Executable Form”
34
          means any form of the work other than Source Code Form.
35
36
   1.7. “Larger Work”
37
          means a work that combines Covered Software with other material,
38
          in a separate file or files, that is not Covered Software.
39
40
   1.8. “License”
41
          means this document.
42
43
   1.9. “Licensable”
44
          means having the right to grant, to the maximum extent possible,
45
          whether at the time of the initial grant or subsequently, any
46
          and all of the rights conveyed by this License.
47
48
   1.10. “Modifications”
49
          means any of the following:
50
51
         a. any file in Source Code Form that results from an addition to,
52
            deletion from, or modification of the contents of Covered
53
            Software; or
54
         b. any new file in Source Code Form that contains any Covered
55
            Software.
56
57
   1.11. “Patent Claims” of a Contributor
58
          means any patent claim(s), including without limitation, method,
59
          process, and apparatus claims, in any patent Licensable by such
60
          Contributor that would be infringed, but for the grant of the
61
          License, by the making, using, selling, offering for sale,
62
          having made, import, or transfer of either its Contributions or
63
          its Contributor Version.
64
65
   1.12. “Secondary License”
66
          means either the GNU General Public License, Version 2.0, the
67
          GNU Lesser General Public License, Version 2.1, the GNU Affero
68
          General Public License, Version 3.0, or any later versions of
69
          those licenses.
70
71
   1.13. “Source Code Form”
72
          means the form of the work preferred for making modifications.
73
74
   1.14. “You” (or “Your”)
75
          means an individual or a legal entity exercising rights under
76
          this License. For legal entities, “You” includes any entity that
77
          controls, is controlled by, or is under common control with You.
78
          For purposes of this definition, “control” means (a) the power,
79
          direct or indirect, to cause the direction or management of such
80
          entity, whether by contract or otherwise, or (b) ownership of
81
          more than fifty percent (50%) of the outstanding shares or
82
          beneficial ownership of such entity.
83
84
2. License Grants and Conditions
85
86
  2.1. Grants
87
88
   Each Contributor hereby grants You a world-wide, royalty-free,
89
   non-exclusive license:
90
    a. under intellectual property rights (other than patent or trademark)
91
       Licensable by such Contributor to use, reproduce, make available,
92
       modify, display, perform, distribute, and otherwise exploit its
93
       Contributions, either on an unmodified basis, with Modifications,
94
       or as part of a Larger Work; and
95
    b. under Patent Claims of such Contributor to make, use, sell, offer
96
       for sale, have made, import, and otherwise transfer either its
97
       Contributions or its Contributor Version.
98
99
  2.2. Effective Date
100
101
   The licenses granted in Section 2.1 with respect to any Contribution
102
   become effective for each Contribution on the date the Contributor
103
   first distributes such Contribution.
104
105
  2.3. Limitations on Grant Scope
106
107
   The licenses granted in this Section 2 are the only rights granted
108
   under this License. No additional rights or licenses will be implied
109
   from the distribution or licensing of Covered Software under this
110
   License. Notwithstanding Section 2.1(b) above, no patent license is
111
   granted by a Contributor:
112
    a. for any code that a Contributor has removed from Covered Software;
113
       or
114
    b. for infringements caused by: (i) Your and any other third party’s
115
       modifications of Covered Software, or (ii) the combination of its
116
       Contributions with other software (except as part of its
117
       Contributor Version); or
118
    c. under Patent Claims infringed by Covered Software in the absence of
119
       its Contributions.
120
121
   This License does not grant any rights in the trademarks, service
122
   marks, or logos of any Contributor (except as may be necessary to
123
   comply with the notice requirements in Section 3.4).
124
125
  2.4. Subsequent Licenses
126
127
   No Contributor makes additional grants as a result of Your choice to
128
   distribute the Covered Software under a subsequent version of this
129
   License (see Section 10.2) or under the terms of a Secondary License
130
   (if permitted under the terms of Section 3.3).
131
132
  2.5. Representation
133
134
   Each Contributor represents that the Contributor believes its
135
   Contributions are its original creation(s) or it has sufficient rights
136
   to grant the rights to its Contributions conveyed by this License.
137
138
  2.6. Fair Use
139
140
   This License is not intended to limit any rights You have under
141
   applicable copyright doctrines of fair use, fair dealing, or other
142
   equivalents.
143
144
  2.7. Conditions
145
146
   Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
147
   in Section 2.1.
148
149
3. Responsibilities
150
151
  3.1. Distribution of Source Form
152
153
   All distribution of Covered Software in Source Code Form, including any
154
   Modifications that You create or to which You contribute, must be under
155
   the terms of this License. You must inform recipients that the Source
156
   Code Form of the Covered Software is governed by the terms of this
157
   License, and how they can obtain a copy of this License. You may not
158
   attempt to alter or restrict the recipients’ rights in the Source Code
159
   Form.
160
161
  3.2. Distribution of Executable Form
162
163
   If You distribute Covered Software in Executable Form then:
164
    a. such Covered Software must also be made available in Source Code
165
       Form, as described in Section 3.1, and You must inform recipients
166
       of the Executable Form how they can obtain a copy of such Source
167
       Code Form by reasonable means in a timely manner, at a charge no
168
       more than the cost of distribution to the recipient; and
169
    b. You may distribute such Executable Form under the terms of this
170
       License, or sublicense it under different terms, provided that the
171
       license for the Executable Form does not attempt to limit or alter
172
       the recipients’ rights in the Source Code Form under this License.
173
174
  3.3. Distribution of a Larger Work
175
176
   You may create and distribute a Larger Work under terms of Your choice,
177
   provided that You also comply with the requirements of this License for
178
   the Covered Software. If the Larger Work is a combination of Covered
179
   Software with a work governed by one or more Secondary Licenses, and
180
   the Covered Software is not Incompatible With Secondary Licenses, this
181
   License permits You to additionally distribute such Covered Software
182
   under the terms of such Secondary License(s), so that the recipient of
183
   the Larger Work may, at their option, further distribute the Covered
184
   Software under the terms of either this License or such Secondary
185
   License(s).
186
187
  3.4. Notices
188
189
   You may not remove or alter the substance of any license notices
190
   (including copyright notices, patent notices, disclaimers of warranty,
191
   or limitations of liability) contained within the Source Code Form of
192
   the Covered Software, except that You may alter any license notices to
193
   the extent required to remedy known factual inaccuracies.
194
195
  3.5. Application of Additional Terms
196
197
   You may choose to offer, and to charge a fee for, warranty, support,
198
   indemnity or liability obligations to one or more recipients of Covered
199
   Software. However, You may do so only on Your own behalf, and not on
200
   behalf of any Contributor. You must make it absolutely clear that any
201
   such warranty, support, indemnity, or liability obligation is offered
202
   by You alone, and You hereby agree to indemnify every Contributor for
203
   any liability incurred by such Contributor as a result of warranty,
204
   support, indemnity or liability terms You offer. You may include
205
   additional disclaimers of warranty and limitations of liability
206
   specific to any jurisdiction.
207
208
4. Inability to Comply Due to Statute or Regulation
209
210
   If it is impossible for You to comply with any of the terms of this
211
   License with respect to some or all of the Covered Software due to
212
   statute, judicial order, or regulation then You must: (a) comply with
213
   the terms of this License to the maximum extent possible; and (b)
214
   describe the limitations and the code they affect. Such description
215
   must be placed in a text file included with all distributions of the
216
   Covered Software under this License. Except to the extent prohibited by
217
   statute or regulation, such description must be sufficiently detailed
218
   for a recipient of ordinary skill to be able to understand it.
219
220
5. Termination
221
222
   5.1. The rights granted under this License will terminate automatically
223
   if You fail to comply with any of its terms. However, if You become
224
   compliant, then the rights granted under this License from a particular
225
   Contributor are reinstated (a) provisionally, unless and until such
226
   Contributor explicitly and finally terminates Your grants, and (b) on
227
   an ongoing basis, if such Contributor fails to notify You of the
228
   non-compliance by some reasonable means prior to 60 days after You have
229
   come back into compliance. Moreover, Your grants from a particular
230
   Contributor are reinstated on an ongoing basis if such Contributor
231
   notifies You of the non-compliance by some reasonable means, this is
232
   the first time You have received notice of non-compliance with this
233
   License from such Contributor, and You become compliant prior to 30
234
   days after Your receipt of the notice.
235
236
   5.2. If You initiate litigation against any entity by asserting a
237
   patent infringement claim (excluding declaratory judgment actions,
238
   counter-claims, and cross-claims) alleging that a Contributor Version
239
   directly or indirectly infringes any patent, then the rights granted to
240
   You by any and all Contributors for the Covered Software under
241
   Section 2.1 of this License shall terminate.
242
243
   5.3. In the event of termination under Sections 5.1 or 5.2 above, all
244
   end user license agreements (excluding distributors and resellers)
245
   which have been validly granted by You or Your distributors under this
246
   License prior to termination shall survive termination.
247
248
6. Disclaimer of Warranty
249
250
   Covered Software is provided under this License on an “as is” basis,
251
   without warranty of any kind, either expressed, implied, or statutory,
252
   including, without limitation, warranties that the Covered Software is
253
   free of defects, merchantable, fit for a particular purpose or
254
   non-infringing. The entire risk as to the quality and performance of
255
   the Covered Software is with You. Should any Covered Software prove
256
   defective in any respect, You (not any Contributor) assume the cost of
257
   any necessary servicing, repair, or correction. This disclaimer of
258
   warranty constitutes an essential part of this License. No use of any
259
   Covered Software is authorized under this License except under this
260
   disclaimer.
261
262
7. Limitation of Liability
263
264
   Under no circumstances and under no legal theory, whether tort
265
   (including negligence), contract, or otherwise, shall any Contributor,
266
   or anyone who distributes Covered Software as permitted above, be
267
   liable to You for any direct, indirect, special, incidental, or
268
   consequential damages of any character including, without limitation,
269
   damages for lost profits, loss of goodwill, work stoppage, computer
270
   failure or malfunction, or any and all other commercial damages or
271
   losses, even if such party shall have been informed of the possibility
272
   of such damages. This limitation of liability shall not apply to
273
   liability for death or personal injury resulting from such party’s
274
   negligence to the extent applicable law prohibits such limitation. Some
275
   jurisdictions do not allow the exclusion or limitation of incidental or
276
   consequential damages, so this exclusion and limitation may not apply
277
   to You.
278
279
8. Litigation
280
281
   Any litigation relating to this License may be brought only in the
282
   courts of a jurisdiction where the defendant maintains its principal
283
   place of business and such litigation shall be governed by laws of that
284
   jurisdiction, without reference to its conflict-of-law provisions.
285
   Nothing in this Section shall prevent a party’s ability to bring
286
   cross-claims or counter-claims.
287
288
9. Miscellaneous
289
290
   This License represents the complete agreement concerning the subject
291
   matter hereof. If any provision of this License is held to be
292
   unenforceable, such provision shall be reformed only to the extent
293
   necessary to make it enforceable. Any law or regulation which provides
294
   that the language of a contract shall be construed against the drafter
295
   shall not be used to construe this License against a Contributor.
296
297
10. Versions of the License
298
299
  10.1. New Versions
300
301
   Mozilla Foundation is the license steward. Except as provided in
302
   Section 10.3, no one other than the license steward has the right to
303
   modify or publish new versions of this License. Each version will be
304
   given a distinguishing version number.
305
306
  10.2. Effect of New Versions
307
308
   You may distribute the Covered Software under the terms of the version
309
   of the License under which You originally received the Covered
310
   Software, or under the terms of any subsequent version published by the
311
   license steward.
312
313
  10.3. Modified Versions
314
315
   If you create software not governed by this License, and you want to
316
   create a new license for such software, you may create and use a
317
   modified version of this License if you rename the license and remove
318
   any references to the name of the license steward (except to note that
319
   such modified license differs from this License).
320
321
  10.4. Distributing Source Code Form that is Incompatible With Secondary
322
  Licenses
323
324
   If You choose to distribute Source Code Form that is Incompatible With
325
   Secondary Licenses under the terms of this version of the License, the
326
   notice described in Exhibit B of this License must be attached.
327
328
Exhibit A - Source Code Form License Notice
329
330
     This Source Code Form is subject to the terms of the Mozilla Public
331
     License, v. 2.0. If a copy of the MPL was not distributed with this
332
     file, You can obtain one at https://mozilla.org/MPL/2.0/.
333
334
   If it is not possible or desirable to put the notice in a particular
335
   file, then You may include the notice in a location (such as a LICENSE
336
   file in a relevant directory) where a recipient would be likely to look
337
   for such a notice.
338
339
   You may add additional accurate notices of copyright ownership.
340
341
Exhibit B - “Incompatible With Secondary Licenses” Notice
342
343
     This Source Code Form is “Incompatible With Secondary Licenses”, as
344
     defined by the Mozilla Public License, v. 2.0.
1345
A licenses/UNDO-FX.md
1
Copyright (c) 2014, TomasMikula
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without modification,
5
are permitted provided that the following conditions are met:
6
7
* Redistributions of source code must retain the above copyright notice, this
8
  list of conditions and the following disclaimer.
9
10
* Redistributions in binary form must reproduce the above copyright notice, this
11
  list of conditions and the following disclaimer in the documentation and/or
12
  other materials provided with the distribution.
113
14
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
18
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
A licenses/WELL-BEHAVED-FX.md
1
Copyright (c) 2014, TomasMikula
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 notice, this
8
  list of conditions and the following disclaimer.
9
10
* Redistributions in binary form must reproduce the above copyright notice,
11
  this list of conditions and the following disclaimer in the documentation
12
  and/or other materials provided with the distribution.
13
14
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
125
A licenses/fonts/FIRACODE.txt
1
Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode)
2
3
This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
This license is copied below, and is also available with a FAQ at:
5
http://scripts.sil.org/OFL
6
7
8
-----------------------------------------------------------
9
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
-----------------------------------------------------------
11
12
PREAMBLE
13
The goals of the Open Font License (OFL) are to stimulate worldwide
14
development of collaborative font projects, to support the font creation
15
efforts of academic and linguistic communities, and to provide a free and
16
open framework in which fonts may be shared and improved in partnership
17
with others.
18
19
The OFL allows the licensed fonts to be used, studied, modified and
20
redistributed freely as long as they are not sold by themselves. The
21
fonts, including any derivative works, can be bundled, embedded,
22
redistributed and/or sold with any software provided that any reserved
23
names are not used by derivative works. The fonts and derivatives,
24
however, cannot be released under any other type of license. The
25
requirement for fonts to remain under this license does not apply
26
to any document created using the fonts or their derivatives.
27
28
DEFINITIONS
29
"Font Software" refers to the set of files released by the Copyright
30
Holder(s) under this license and clearly marked as such. This may
31
include source files, build scripts and documentation.
32
33
"Reserved Font Name" refers to any names specified as such after the
34
copyright statement(s).
35
36
"Original Version" refers to the collection of Font Software components as
37
distributed by the Copyright Holder(s).
38
39
"Modified Version" refers to any derivative made by adding to, deleting,
40
or substituting -- in part or in whole -- any of the components of the
41
Original Version, by changing formats or by porting the Font Software to a
42
new environment.
43
44
"Author" refers to any designer, engineer, programmer, technical
45
writer or other person who contributed to the Font Software.
46
47
PERMISSION & CONDITIONS
48
Permission is hereby granted, free of charge, to any person obtaining
49
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
redistribute, and sell modified and unmodified copies of the Font
51
Software, subject to the following conditions:
52
53
1) Neither the Font Software nor any of its individual components,
54
in Original or Modified Versions, may be sold by itself.
55
56
2) Original or Modified Versions of the Font Software may be bundled,
57
redistributed and/or sold with any software, provided that each copy
58
contains the above copyright notice and this license. These can be
59
included either as stand-alone text files, human-readable headers or
60
in the appropriate machine-readable metadata fields within text or
61
binary files as long as those fields can be easily viewed by the user.
62
63
3) No Modified Version of the Font Software may use the Reserved Font
64
Name(s) unless explicit written permission is granted by the corresponding
65
Copyright Holder. This restriction only applies to the primary font name as
66
presented to the users.
67
68
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
Software shall not be used to promote, endorse or advertise any
70
Modified Version, except to acknowledge the contribution(s) of the
71
Copyright Holder(s) and the Author(s) or with their explicit written
72
permission.
73
74
5) The Font Software, modified or unmodified, in part or in whole,
75
must be distributed entirely under this license, and must not be
76
distributed under any other license. The requirement for fonts to
77
remain under this license does not apply to any document created
78
using the Font Software.
79
80
TERMINATION
81
This license becomes null and void if any of the above conditions are
82
not met.
83
84
DISCLAIMER
85
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
OTHER DEALINGS IN THE FONT SOFTWARE.
194
A licenses/fonts/VOLLKORN.txt
1
Copyright 2017 The Vollkorn Project Authors (https://github.com/FAlthausen/Vollkorn-Typeface)
2
3
This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
This license is copied below, and is also available with a FAQ at:
5
http://scripts.sil.org/OFL
6
7
8
-----------------------------------------------------------
9
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
-----------------------------------------------------------
11
12
PREAMBLE
13
The goals of the Open Font License (OFL) are to stimulate worldwide
14
development of collaborative font projects, to support the font creation
15
efforts of academic and linguistic communities, and to provide a free and
16
open framework in which fonts may be shared and improved in partnership
17
with others.
18
19
The OFL allows the licensed fonts to be used, studied, modified and
20
redistributed freely as long as they are not sold by themselves. The
21
fonts, including any derivative works, can be bundled, embedded, 
22
redistributed and/or sold with any software provided that any reserved
23
names are not used by derivative works. The fonts and derivatives,
24
however, cannot be released under any other type of license. The
25
requirement for fonts to remain under this license does not apply
26
to any document created using the fonts or their derivatives.
27
28
DEFINITIONS
29
"Font Software" refers to the set of files released by the Copyright
30
Holder(s) under this license and clearly marked as such. This may
31
include source files, build scripts and documentation.
32
33
"Reserved Font Name" refers to any names specified as such after the
34
copyright statement(s).
35
36
"Original Version" refers to the collection of Font Software components as
37
distributed by the Copyright Holder(s).
38
39
"Modified Version" refers to any derivative made by adding to, deleting,
40
or substituting -- in part or in whole -- any of the components of the
41
Original Version, by changing formats or by porting the Font Software to a
42
new environment.
43
44
"Author" refers to any designer, engineer, programmer, technical
45
writer or other person who contributed to the Font Software.
46
47
PERMISSION & CONDITIONS
48
Permission is hereby granted, free of charge, to any person obtaining
49
a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
redistribute, and sell modified and unmodified copies of the Font
51
Software, subject to the following conditions:
52
53
1) Neither the Font Software nor any of its individual components,
54
in Original or Modified Versions, may be sold by itself.
55
56
2) Original or Modified Versions of the Font Software may be bundled,
57
redistributed and/or sold with any software, provided that each copy
58
contains the above copyright notice and this license. These can be
59
included either as stand-alone text files, human-readable headers or
60
in the appropriate machine-readable metadata fields within text or
61
binary files as long as those fields can be easily viewed by the user.
62
63
3) No Modified Version of the Font Software may use the Reserved Font
64
Name(s) unless explicit written permission is granted by the corresponding
65
Copyright Holder. This restriction only applies to the primary font name as
66
presented to the users.
67
68
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
Software shall not be used to promote, endorse or advertise any
70
Modified Version, except to acknowledge the contribution(s) of the
71
Copyright Holder(s) and the Author(s) or with their explicit written
72
permission.
73
74
5) The Font Software, modified or unmodified, in part or in whole,
75
must be distributed entirely under this license, and must not be
76
distributed under any other license. The requirement for fonts to
77
remain under this license does not apply to any document created
78
using the Font Software.
79
80
TERMINATION
81
This license becomes null and void if any of the above conditions are
82
not met.
83
84
DISCLAIMER
85
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
OTHER DEALINGS IN THE FONT SOFTWARE.
194
A scripts/.gitignore
1
*.class
12
A scripts/01.sikuli/01.py
1
# -----------------------------------------------------------------------------
2
# Copyright 2020 White Magic Software, Ltd.
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining a
5
# copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# This script introduces the editor and its purpose.
26
# -----------------------------------------------------------------------------
27
import sys
28
29
if not "../editor.sikuli" in sys.path:
30
    sys.path.append( "../editor.sikuli" )
31
32
from editor import *
33
34
# ---------------------------------------------------------------
35
# Fresh start
36
# ---------------------------------------------------------------
37
rm( app_home + "/variables.yaml" )
38
rm( app_home + "/untitled.md" )
39
rm( home + "/.scrivenvar" )
40
41
# ---------------------------------------------------------------
42
# Wait for application to launch
43
# ---------------------------------------------------------------
44
openApp( "java -jar " + app_bin )
45
46
wait("1594187265140.png", 30)
47
# ---------------------------------------------------------------
48
# Introduction
49
# ---------------------------------------------------------------
50
set_typing_speed( 240 )
51
52
header( "What is this application?" )
53
typer( "Well, this application is a text editor that supports interpolated definitions, ")
54
typer( "a few different text formats, real-time preview, spell check ") 
55
typer( "as you tipe" ) 
56
wait( 0.5 )
57
recur( 3, backspace )
58
typer( "ype, and R statements." )
59
paragraph()
60
wait( 1 )
61
62
# ---------------------------------------------------------------
63
# Definition demo
64
# ---------------------------------------------------------------
65
header( "What are definitions?" )
66
typer( "Watch. " )
67
wait( .5 )
68
69
# Focus the definition editor.
70
click_create()
71
recur( 4, tab )
72
73
wait( .5 )
74
rename_definition( "application" )
75
76
insert()
77
rename_definition( "title" )
78
79
insert()
80
rename_definition( "Scrivenvar" )
81
82
# Set focus to the text editor.
83
tab()
84
85
typer( "The left-hand pane contains a nested, folder-like structure of names " )
86
typer( "and values that are called *definitions*. " )
87
wait( .5 )
88
typer( "Such definitions can simplify updating documents. " )
89
wait( 1 )
90
91
edit_find( "this application" )
92
typer( "$application.title$" )
93
94
edit_find_next()
95
typer( "$application.title$" )
96
97
type( Key.END, Key.CTRL )
98
99
typer( "The right-hand pane shows the result after having substituted definition " )
100
typer( "values into the document." )
101
wait( 2 )
102
paragraph()
103
104
header( "What is interpolation?" )
105
typer( "Definition values can reference definition names. " )
106
wait( .5 )
107
typer( "The definition names act as placeholders. Substituting placeholders with " )
108
typer( "their defined value is called *interpolation*. Let's see how it works." )
109
wait( 2 )
1110
A scripts/01.sikuli/1594187265140.png
Binary file
A scripts/02.sikuli/02.py
1
# -----------------------------------------------------------------------------
2
# Copyright 2020 White Magic Software, Ltd.
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining a
5
# copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# This script demonstrates how to use interpolated strings.
26
# -----------------------------------------------------------------------------
27
import sys
28
29
if not "../editor.sikuli" in sys.path:
30
    sys.path.append( "../editor.sikuli" )
31
32
from editor import *
33
34
# -----------------------------------------------------------------------------
35
# Open sample chapter.
36
# -----------------------------------------------------------------------------
37
file_open()
38
type( Key.UP, Key.ALT )
39
wait( 1 )
40
typer( Key.END )
41
wait( 1 )
42
enter()
43
wait( 0.5 )
44
enter()
45
wait( 1 )
46
47
# -----------------------------------------------------------------------------
48
# Open the corresponding definition file.
49
# -----------------------------------------------------------------------------
50
file_open()
51
recur( 2, down )
52
wait( 1 )
53
enter()
54
wait( 1 )
55
56
# -----------------------------------------------------------------------------
57
# Edit the sample document.
58
# -----------------------------------------------------------------------------
59
set_typing_speed( 80 )
60
61
type( Key.HOME, Key.CTRL )
62
recur( 2, down )
63
64
# Grey
65
recur( 3, skip_right )
66
autoinsert()
67
68
# 34
69
recur( 4, skip_right )
70
autoinsert()
71
72
# Central
73
recur( 10, skip_right )
74
autoinsert()
75
76
# London
77
skip_right()
78
autoinsert()
79
80
# Hatchery
81
skip_right()
82
autoinsert()
83
84
# and Conditioning
85
recur( 2, select_word_right )
86
delete()
87
88
# Centre
89
skip_right()
90
autoinsert()
91
92
set_typing_speed( 220 )
93
94
typer( " Let's interpolate those four definitions instead!" )
95
wait( 4 )
96
recur( 13, type, Key.BACKSPACE, Key.CTRL )
97
recur( 9, backspace )
98
99
set_typing_speed( 60 )
100
101
typer( "name$" )
102
wait( 2 )
103
104
# Collapse all definitions
105
tab()
106
recur( 8, typer, Key.LEFT )
107
108
# Expand to city
109
recur( 4, typer, Key.RIGHT )
110
111
# Jump to name
112
recur( 2, down )
113
recur( 2, typer, Key.RIGHT )
114
115
# Open the text field to show the full value
116
typer( Key.F2 )
117
118
# Traverse the text field
119
home()
120
recur( 16, type, Key.RIGHT, Key.CTRL )
121
esc()
122
123
restore_typing_speed()
124
125
tab()
126
type( Key.HOME, Key.CTRL )
127
edit_find( "Director" )
128
autoinsert()
129
130
edit_find_next()
131
autoinsert()
132
133
edit_find_next()
134
typer( Key.RIGHT )
135
recur( 2, delete )
136
autoinsert()
137
typer( "'s" )
138
139
edit_find( "Hatcheries" )
140
autoinsert()
141
142
# and Conditioning
143
recur( 2, select_word_right )
144
delete()
145
146
edit_find( "Central" )
147
autoinsert()
148
149
skip_right()
150
autoinsert()
151
152
typer( " How about a different city?" )
153
wait( 2 )
154
recur( 5, type, Key.BACKSPACE, Key.CTRL )
155
wait( 1 )
156
tab()
157
typer( Key.F2 )
158
typer( "Seattle" )
159
enter()
160
tab()
161
wait( 2 )
162
163
type( Key.END, Key.CTRL )
164
paragraph()
165
typer( "No?" )
166
paragraph()
167
168
tab()
169
typer( Key.F2 )
170
typer( "London" )
171
enter()
172
173
tab()
174
typer( "Organizing definitions is left to your ")
175
typer( "doub" )
176
wait( .25 )
177
autoinsert()
178
wait( 1 )
179
typer( " Good imagination." )
180
tab()
181
182
# Jump to "char" definition
183
home()
184
185
# Jump to "char.a.primary.name" definition
186
recur( 6, typer, Key.RIGHT )
187
188
# Jump to "char.a.primary.caste" definition
189
down()
190
typer( Key.RIGHT )
191
192
# Jump to root-level "caste" definition
193
recur( 7, down )
194
195
# Reselect "super"
196
recur( 5, typer, Key.RIGHT )
197
wait( 2 )
198
199
# Close the window, no save
200
type( "w", Key.CTRL )
201
wait( 0.5 )
202
tab()
203
wait( 0.5 )
204
typer( Key.SPACE )
205
wait( 1 )
1206
A scripts/03.sikuli/03.py
1
# -----------------------------------------------------------------------------
2
# Copyright 2020 White Magic Software, Ltd.
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining a
5
# copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# This script introduces images and R.
26
# -----------------------------------------------------------------------------
27
import sys
28
29
if not "../editor.sikuli" in sys.path:
30
    sys.path.append( "../editor.sikuli" )
31
32
from editor import *
33
34
set_typing_speed( 80 )
35
file_open()
36
type( Key.UP, Key.ALT )
37
wait( 0.5 )
38
home()
39
wait( 0.25 )
40
enter()
41
wait( 1 )
42
end()
43
wait( 0.25 )
44
enter()
45
wait( 1 )
46
47
set_typing_speed( 200 )
48
49
paragraph()
50
header( "What text formats are supported?" )
51
52
typer( "Scr" )
53
autoinsert()
54
typer( " supports Markdown, R Markdown, XML, and R XML; however, the software " )
55
typer( "architecture enables it to easily add new formats. The following figure " )
56
typer( "depicts the overall architecture: " )
57
paragraph()
58
typer( "![](../writing/images/architecture)" )
59
paragraph()
60
typer( "Most text editors read a single format and convert it to one other format. " )
61
typer( "With a little more effort, text editors can support many input and output " )
62
typer( "formats. Scr" )
63
autoinsert()
64
typer( " goes one step further by introducing interpolated definitions." )
65
paragraph()
66
typer( "Kitten interlude:" )
67
paragraph()
68
typer( "![](https://i.imgur.com/jboueQH.jpg)" )
69
paragraph()
70
71
header( "What is R?" )
72
typer( "R is a programming language. You might have noticed a few potential grammar " )
73
typer( "problems with direct substitution. Rules for possessive forms, numbers, and " )
74
typer( "other linguistic exceptions can be addressed using R. Let's take a look!" )
175
A scripts/04.sikuli/04.py
1
# -----------------------------------------------------------------------------
2
# Copyright 2020 White Magic Software, Ltd.
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining a
5
# copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# This script demonstrates using R.
26
# -----------------------------------------------------------------------------
27
import sys
28
29
if not "../editor.sikuli" in sys.path:
30
    sys.path.append( "../editor.sikuli" )
31
32
from editor import *
33
34
# -----------------------------------------------------------------------------
35
# Open the demo text.
36
# -----------------------------------------------------------------------------
37
file_open()
38
type( Key.UP, Key.ALT )
39
wait( 0.5 )
40
end()
41
wait( 0.25 )
42
enter()
43
wait( 0.5 )
44
down()
45
wait( 0.25 )
46
enter()
47
wait( 1 )
48
49
# -----------------------------------------------------------------------------
50
# Demo bootstrapping
51
# -----------------------------------------------------------------------------
52
53
# Jump to the end.
54
type( Key.END, Key.CTRL )
55
paragraph()
56
57
type( "s", Key.CTRL + Key.ALT )
58
wait( 0.25 )
59
60
61
# -----------------------------------------------------------------------------
62
# Demo pluralization
63
# -----------------------------------------------------------------------------
64
65
66
# -----------------------------------------------------------------------------
67
# Demo possessives (it, she, he, Ross)
68
# -----------------------------------------------------------------------------
69
70
71
# -----------------------------------------------------------------------------
72
# Demo conversion, including ordinal numbers
73
# -----------------------------------------------------------------------------
74
75
# -----------------------------------------------------------------------------
76
# Demo Chicago Manual of Style
77
# -----------------------------------------------------------------------------
78
79
80
# -----------------------------------------------------------------------------
81
# Demo CSV file import
82
# -----------------------------------------------------------------------------
83
84
185
A scripts/editor.sikuli/1594187923258.png
Binary file
A scripts/editor.sikuli/editor.py
1
# -----------------------------------------------------------------------------
2
# Copyright 2020 White Magic Software, Ltd.
3
#
4
# Permission is hereby granted, free of charge, to any person obtaining a
5
# copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
#
12
# The above copyright notice and this permission notice shall be included
13
# in all copies or substantial portions of the Software.
14
#
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# This script contains helper functions used by the other scripts.
26
#
27
# Do not run this script.
28
# -----------------------------------------------------------------------------
29
30
from sikuli import *
31
import sys
32
import os
33
from os.path import expanduser
34
35
home = expanduser( "~" )
36
app_home = home + "/bin"
37
app_bin = app_home + "/scrivenvar.jar"
38
39
wpm_default_speed = 80
40
wpm_typing_speed = wpm_default_speed
41
42
# -----------------------------------------------------------------------------
43
# Try to delete the file pointed to by the path variable. If there is no such
44
# file, this will silently ignore the exception.
45
# -----------------------------------------------------------------------------
46
def rm( path ):
47
    try:
48
        os.remove( path )
49
    except:
50
        print "Ignored"
51
52
# -----------------------------------------------------------------------------
53
# Changes the current typing speed, where speed is given in words per minute.
54
# -----------------------------------------------------------------------------
55
def set_typing_speed( wpm ):
56
    global wpm_typing_speed
57
    wpm_typing_speed = wpm
58
59
def restore_typing_speed():
60
    set_typing_speed( wpm_default_speed )
61
62
# -----------------------------------------------------------------------------
63
# Creates a delay between keystrokes to emulate typing at a particular speed.
64
# -----------------------------------------------------------------------------
65
def random_wait():
66
    from time import sleep
67
    from random import uniform
68
    cpm = wpm_typing_speed * 5.1
69
    cps = cpm / 60.0
70
    ms_per_char = 1000.0 / cps
71
    ms_per_stroke = ms_per_char / 2.0
72
73
    noise = uniform( 0, ms_per_stroke / 2 )
74
    duration = (ms_per_stroke + noise ) / 1000
75
    
76
    sleep( duration )
77
78
# -----------------------------------------------------------------------------
79
# Repeats a function call, f, n times.
80
# -----------------------------------------------------------------------------
81
def recur( n, f, *args ):
82
    for i in range( n ):
83
        f(*args)
84
        random_wait()
85
86
# -----------------------------------------------------------------------------
87
# Emulate a typist who is typing in the given text.
88
# -----------------------------------------------------------------------------
89
def typer( text ):
90
    # ~25 is a reasonably realistic, fast typist.
91
    for c in text:
92
        type( c )
93
        random_wait()
94
95
# -----------------------------------------------------------------------------
96
# Injects a definition.
97
# -----------------------------------------------------------------------------
98
def autoinsert():
99
    type( Key.SPACE, Key.CTRL )
100
    random_wait()
101
102
# -----------------------------------------------------------------------------
103
# Types the TAB key.
104
# -----------------------------------------------------------------------------
105
def tab():
106
    typer( Key.TAB )
107
108
# -----------------------------------------------------------------------------
109
# Types the ENTER key.
110
# -----------------------------------------------------------------------------
111
def enter():
112
    typer( Key.ENTER )
113
114
# -----------------------------------------------------------------------------
115
# Types the ESC key.
116
# -----------------------------------------------------------------------------
117
def esc():
118
    typer( Key.ESC )
119
120
# -----------------------------------------------------------------------------
121
# Types the DOWN arrow key.
122
# -----------------------------------------------------------------------------
123
def down():
124
    typer( Key.DOWN )
125
126
# -----------------------------------------------------------------------------
127
# Types the HOME key.
128
# -----------------------------------------------------------------------------
129
def home():
130
    typer( Key.HOME )
131
132
# -----------------------------------------------------------------------------
133
# Types the END key.
134
# -----------------------------------------------------------------------------
135
def end():
136
    typer( Key.END )
137
138
# -----------------------------------------------------------------------------
139
# Types the BACKSPACE key.
140
# -----------------------------------------------------------------------------
141
def backspace():
142
    typer( Key.BACKSPACE )
143
144
# -----------------------------------------------------------------------------
145
# Types the INSERT key, often to insert a new definition.
146
# -----------------------------------------------------------------------------
147
def insert():
148
    typer( Key.INSERT )
149
150
# -----------------------------------------------------------------------------
151
# Types the DELETE key, often to remove selected text.
152
# -----------------------------------------------------------------------------
153
def delete():
154
    typer( Key.DELETE )
155
156
# -----------------------------------------------------------------------------
157
# Moves the cursor one word to the right.
158
# -----------------------------------------------------------------------------
159
def skip_right():
160
    type( Key.RIGHT, Key.CTRL )
161
    random_wait()
162
163
def select_word_right():
164
    type( Key.RIGHT, Key.CTRL + Key.SHIFT )
165
    random_wait()
166
167
# -----------------------------------------------------------------------------
168
# Types ENTER twice to begin a new paragraph.
169
# -----------------------------------------------------------------------------
170
def paragraph():
171
    recur( 2, enter )
172
    wait( 1.5 )
173
174
# -----------------------------------------------------------------------------
175
# Writes a heading to the document using the given text value as the content.
176
# -----------------------------------------------------------------------------
177
def header( text ):
178
    typer( "# " + text )
179
    paragraph()
180
181
# -----------------------------------------------------------------------------
182
# Clicks the "Create" button to add a new definition.
183
# -----------------------------------------------------------------------------
184
def click_create():
185
    click("1594187923258.png")
186
    wait( .5 )
187
188
# -----------------------------------------------------------------------------
189
# Changes the text for the actively selected definition.
190
# -----------------------------------------------------------------------------
191
def rename_definition( text ):
192
    typer( Key.F2 )
193
    typer( text )
194
    enter()
195
    wait( .5 )
196
197
# -----------------------------------------------------------------------------
198
# Searches for the given text within the document.
199
# -----------------------------------------------------------------------------
200
def edit_find( text ):
201
    type( "f", Key.CTRL )
202
    typer( text )
203
    enter()
204
    wait( .25 )
205
    esc()
206
    wait( .5 )
207
208
# -----------------------------------------------------------------------------
209
# Searches for the next occurrence of the previous search term.
210
# -----------------------------------------------------------------------------
211
def edit_find_next():
212
    typer( Key.F3 )
213
    wait( .5 )
214
215
# -----------------------------------------------------------------------------
216
# Opens a dialog for selecting a file.
217
# -----------------------------------------------------------------------------
218
def file_open():
219
    type( "o", Key.CTRL )
220
    wait( 1 )
1221
A settings.gradle
11
A src/main/java/com/scrivenvar/AbstractFileFactory.java
1
/*
2
 * Copyright 2020 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.predicates.files.FileTypePredicate;
31
import com.scrivenvar.service.Settings;
32
33
import java.nio.file.Path;
34
import java.util.Iterator;
35
import java.util.List;
36
37
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
38
import static com.scrivenvar.FileType.UNKNOWN;
39
import static java.lang.String.format;
40
41
/**
42
 * Provides common behaviours for factories that instantiate classes based on
43
 * file type.
44
 */
45
public class AbstractFileFactory {
46
47
  private static final String MSG_UNKNOWN_FILE_TYPE =
48
      "Unknown type '%s' for file '%s'.";
49
50
  private final Settings mSettings = Services.load( Settings.class );
51
52
  /**
53
   * Determines the file type from the path extension. This should only be
54
   * called when it is known that the file type won't be a definition file
55
   * (e.g., YAML or other definition source), but rather an editable file
56
   * (e.g., Markdown, XML, etc.).
57
   *
58
   * @param path The path with a file name extension.
59
   * @return The FileType for the given path.
60
   */
61
  public FileType lookup( final Path path ) {
62
    return lookup( path, GLOB_PREFIX_FILE );
63
  }
64
65
  /**
66
   * Creates a file type that corresponds to the given path.
67
   *
68
   * @param path   Reference to a variable definition file.
69
   * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
70
   * @return The file type that corresponds to the given path.
71
   */
72
  protected FileType lookup( final Path path, final String prefix ) {
73
    assert path != null;
74
    assert prefix != null;
75
76
    final Settings properties = getSettings();
77
    final Iterator<String> keys = properties.getKeys( prefix );
78
79
    boolean found = false;
80
    FileType fileType = UNKNOWN;
81
82
    while( keys.hasNext() && !found ) {
83
      final String key = keys.next();
84
      final List<String> patterns = properties.getStringSettingList( key );
85
      final FileTypePredicate predicate = new FileTypePredicate( patterns );
86
87
      if( found = predicate.test( path.toFile() ) ) {
88
        // Remove the EXTENSIONS_PREFIX to get the filename extension mapped
89
        // to a standard name (as defined in the settings.properties file).
90
        final String suffix = key.replace( prefix + ".", "" );
91
        fileType = FileType.from( suffix );
92
      }
93
    }
94
95
    return fileType;
96
  }
97
98
  /**
99
   * Throws IllegalArgumentException because the given path could not be
100
   * recognized. This exists because
101
   *
102
   * @param type The detected path type (protocol, file extension, etc.).
103
   * @param path The path to a source of definitions.
104
   */
105
  protected void unknownFileType( final String type, final String path ) {
106
    final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path );
107
    throw new IllegalArgumentException( msg );
108
  }
109
110
  /**
111
   * Return the singleton Settings instance.
112
   *
113
   * @return A non-null instance.
114
   */
115
  private Settings getSettings() {
116
    return this.mSettings;
117
  }
118
}
1119
A src/main/java/com/scrivenvar/Constants.java
1
/*
2
 * Copyright 2020 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
32
import java.nio.file.Path;
33
import java.nio.file.Paths;
34
35
/**
36
 * Defines application-wide default values.
37
 */
38
public class Constants {
39
40
  private static final Settings SETTINGS = Services.load( Settings.class );
41
42
  /**
43
   * Prevent instantiation.
44
   */
45
  private Constants() {
46
  }
47
48
  private static String get( final String key ) {
49
    return SETTINGS.getSetting( key, "" );
50
  }
51
52
  @SuppressWarnings("SameParameterValue")
53
  private static int get( final String key, final int defaultValue ) {
54
    return SETTINGS.getSetting( key, defaultValue );
55
  }
56
57
  // Bootstrapping...
58
  public static final String SETTINGS_NAME =
59
      "/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
64
  // Prevent double events when updating files on Linux (save and timestamp).
65
  public static final int APP_WATCHDOG_TIMEOUT = get(
66
      "application.watchdog.timeout", 200 );
67
68
  public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
69
  public static final String STYLESHEET_MARKDOWN = get(
70
      "file.stylesheet.markdown" );
71
  public static final String STYLESHEET_PREVIEW = get(
72
      "file.stylesheet.preview" );
73
74
  public static final String FILE_LOGO_16 = get( "file.logo.16" );
75
  public static final String FILE_LOGO_32 = get( "file.logo.32" );
76
  public static final String FILE_LOGO_128 = get( "file.logo.128" );
77
  public static final String FILE_LOGO_256 = get( "file.logo.256" );
78
  public static final String FILE_LOGO_512 = get( "file.logo.512" );
79
80
  public static final String PREFS_ROOT = get( "preferences.root" );
81
  public static final String PREFS_STATE = get( "preferences.root.state" );
82
83
  // Refer to filename extension settings in the configuration file. Do not
84
  // terminate these prefixes with a period.
85
  public static final String GLOB_PREFIX_FILE = "file.ext";
86
  public static final String GLOB_PREFIX_DEFINITION =
87
      "definition." + GLOB_PREFIX_FILE;
88
89
  // Different definition source protocols.
90
  public static final String DEFINITION_PROTOCOL_UNKNOWN = "unknown";
91
  public static final String DEFINITION_PROTOCOL_FILE = "file";
92
93
  // Three parameters: line number, column number, and offset
94
  public static final String STATUS_BAR_LINE = "Main.statusbar.line";
95
96
  // "OK" text
97
  public static final String STATUS_BAR_OK = "Main.statusbar.state.default";
98
  public static final String STATUS_PARSE_ERROR = "Main.statusbar.parse.error";
99
100
  /**
101
   * Used when creating flat maps relating to resolved variables.
102
   */
103
  public static final int DEFAULT_MAP_SIZE = 64;
104
105
  /**
106
   * Default image extension order to use when scanning.
107
   */
108
  public static final String PERSIST_IMAGES_DEFAULT =
109
      get( "file.ext.image.order" );
110
111
  /**
112
   * Default working directory to use for R startup script.
113
   */
114
  public static final String USER_DIRECTORY = System.getProperty( "user.dir" );
115
116
  /**
117
   * Default path to use for an untitled (pathless) file.
118
   */
119
  public static final Path DEFAULT_DIRECTORY = Paths.get( USER_DIRECTORY );
120
121
  /**
122
   * Default starting delimiter when inserting R variables.
123
   */
124
  public static final String R_DELIMITER_BEGAN_DEFAULT = "x( ";
125
126
  /**
127
   * Default ending delimiter when inserting R variables.
128
   */
129
  public static final String R_DELIMITER_ENDED_DEFAULT = " )";
130
131
  /**
132
   * Resource directory where different language lexicons are located.
133
   */
134
  public static final String LEXICONS_DIRECTORY = "lexicons";
135
136
  /**
137
   * Used as the prefix for uniquely identifying HTML block elements, which
138
   * helps coordinate scrolling the preview pane to where the user is typing.
139
   */
140
  public static final String PARAGRAPH_ID_PREFIX = "p-";
141
142
  public static final String FONT_DIRECTORY = "/fonts";
143
}
1144
A src/main/java/com/scrivenvar/FileEditorTab.java
1
/*
2
 * Copyright 2020 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 javafx.beans.binding.Bindings;
33
import javafx.beans.property.BooleanProperty;
34
import javafx.beans.property.ReadOnlyBooleanProperty;
35
import javafx.beans.property.ReadOnlyBooleanWrapper;
36
import javafx.beans.property.SimpleBooleanProperty;
37
import javafx.beans.value.ChangeListener;
38
import javafx.event.Event;
39
import javafx.event.EventHandler;
40
import javafx.event.EventType;
41
import javafx.scene.Scene;
42
import javafx.scene.control.Tab;
43
import javafx.scene.control.Tooltip;
44
import javafx.scene.text.Text;
45
import javafx.stage.Window;
46
import org.fxmisc.flowless.VirtualizedScrollPane;
47
import org.fxmisc.richtext.StyleClassedTextArea;
48
import org.fxmisc.undo.UndoManager;
49
import org.jetbrains.annotations.NotNull;
50
import org.mozilla.universalchardet.UniversalDetector;
51
52
import java.io.File;
53
import java.nio.charset.Charset;
54
import java.nio.file.Files;
55
import java.nio.file.Path;
56
57
import static com.scrivenvar.Messages.get;
58
import static java.nio.charset.StandardCharsets.UTF_8;
59
import static java.util.Locale.ENGLISH;
60
import static javafx.application.Platform.runLater;
61
62
/**
63
 * Editor for a single file.
64
 */
65
public final class FileEditorTab extends Tab {
66
67
  private final Notifier mNotifier = Services.load( Notifier.class );
68
  private final MarkdownEditorPane mEditorPane = new MarkdownEditorPane();
69
70
  private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper();
71
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
72
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
73
74
  /**
75
   * Character encoding used by the file (or default encoding if none found).
76
   */
77
  private Charset mEncoding = UTF_8;
78
79
  /**
80
   * File to load into the editor.
81
   */
82
  private Path mPath;
83
84
  public FileEditorTab( final Path path ) {
85
    setPath( path );
86
87
    mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() );
88
89
    setOnSelectionChanged( e -> {
90
      if( isSelected() ) {
91
        runLater( this::activated );
92
        requestFocus();
93
      }
94
    } );
95
  }
96
97
  private void updateTab() {
98
    setText( getTabTitle() );
99
    setGraphic( getModifiedMark() );
100
    setTooltip( getTabTooltip() );
101
  }
102
103
  /**
104
   * Returns the base filename (without the directory names).
105
   *
106
   * @return The untitled text if the path hasn't been set.
107
   */
108
  private String getTabTitle() {
109
    return getPath().getFileName().toString();
110
  }
111
112
  /**
113
   * Returns the full filename represented by the path.
114
   *
115
   * @return The untitled text if the path hasn't been set.
116
   */
117
  private Tooltip getTabTooltip() {
118
    final Path filePath = getPath();
119
    return new Tooltip( filePath == null ? "" : filePath.toString() );
120
  }
121
122
  /**
123
   * Returns a marker to indicate whether the file has been modified.
124
   *
125
   * @return "*" when the file has changed; otherwise null.
126
   */
127
  private Text getModifiedMark() {
128
    return isModified() ? new Text( "*" ) : null;
129
  }
130
131
  /**
132
   * Called when the user switches tab.
133
   */
134
  private void activated() {
135
    // Tab is closed or no longer active.
136
    if( getTabPane() == null || !isSelected() ) {
137
      return;
138
    }
139
140
    // If the tab is devoid of content, load it.
141
    if( getContent() == null ) {
142
      readFile();
143
      initLayout();
144
      initUndoManager();
145
    }
146
  }
147
148
  private void initLayout() {
149
    setContent( getScrollPane() );
150
  }
151
152
  /**
153
   * Tracks undo requests, but can only be called <em>after</em> load.
154
   */
155
  private void initUndoManager() {
156
    final UndoManager<?> undoManager = getUndoManager();
157
    undoManager.forgetHistory();
158
159
    // Bind the editor undo manager to the properties.
160
    mModified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
161
    canUndo.bind( undoManager.undoAvailableProperty() );
162
    canRedo.bind( undoManager.redoAvailableProperty() );
163
  }
164
165
  private void requestFocus() {
166
    getEditorPane().requestFocus();
167
  }
168
169
  /**
170
   * Searches from the caret position forward for the given string.
171
   *
172
   * @param needle The text string to match.
173
   */
174
  public void searchNext( final String needle ) {
175
    final String haystack = getEditorText();
176
    int index = haystack.indexOf( needle, getCaretPosition() );
177
178
    // Wrap around.
179
    if( index == -1 ) {
180
      index = haystack.indexOf( needle );
181
    }
182
183
    if( index >= 0 ) {
184
      setCaretPosition( index );
185
      getEditor().selectRange( index, index + needle.length() );
186
    }
187
  }
188
189
  /**
190
   * Gets a reference to the scroll pane that houses the editor.
191
   *
192
   * @return The editor's scroll pane, containing a vertical scrollbar.
193
   */
194
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
195
    return getEditorPane().getScrollPane();
196
  }
197
198
  /**
199
   * Returns the index into the text where the caret blinks happily away.
200
   *
201
   * @return A number from 0 to the editor's document text length.
202
   */
203
  public int getCaretPosition() {
204
    return getEditor().getCaretPosition();
205
  }
206
207
  /**
208
   * Moves the caret to a given offset.
209
   *
210
   * @param offset The new caret offset.
211
   */
212
  private void setCaretPosition( final int offset ) {
213
    getEditor().moveTo( offset );
214
    getEditor().requestFollowCaret();
215
  }
216
217
  /**
218
   * Returns the text area associated with this tab.
219
   *
220
   * @return A text editor.
221
   */
222
  private StyleClassedTextArea getEditor() {
223
    return getEditorPane().getEditor();
224
  }
225
226
  /**
227
   * Returns true if the given path exactly matches this tab's path.
228
   *
229
   * @param check The path to compare against.
230
   * @return true The paths are the same.
231
   */
232
  public boolean isPath( final Path check ) {
233
    final Path filePath = getPath();
234
235
    return filePath != null && filePath.equals( check );
236
  }
237
238
  /**
239
   * Reads the entire file contents from the path associated with this tab.
240
   */
241
  private void readFile() {
242
    final Path path = getPath();
243
    final File file = path.toFile();
244
245
    try {
246
      if( file.exists() ) {
247
        if( file.canWrite() && file.canRead() ) {
248
          final EditorPane pane = getEditorPane();
249
          pane.setText( asString( Files.readAllBytes( path ) ) );
250
          pane.scrollToTop();
251
        }
252
        else {
253
          final String msg = get(
254
              "FileEditor.loadFailed.message",
255
              file.toString(),
256
              get( "FileEditor.loadFailed.reason.permissions" )
257
          );
258
          getNotifier().notify( msg );
259
        }
260
      }
261
    } catch( final Exception ex ) {
262
      getNotifier().notify( ex );
263
    }
264
  }
265
266
  /**
267
   * Saves the entire file contents from the path associated with this tab.
268
   *
269
   * @return true The file has been saved.
270
   */
271
  public boolean save() {
272
    try {
273
      final EditorPane editor = getEditorPane();
274
      Files.write( getPath(), asBytes( editor.getText() ) );
275
      editor.getUndoManager().mark();
276
      return true;
277
    } catch( final Exception ex ) {
278
      return alert(
279
          "FileEditor.saveFailed.title",
280
          "FileEditor.saveFailed.message",
281
          ex
282
      );
283
    }
284
  }
285
286
  /**
287
   * Creates an alert dialog and waits for it to close.
288
   *
289
   * @param titleKey   Resource bundle key for the alert dialog title.
290
   * @param messageKey Resource bundle key for the alert dialog message.
291
   * @param e          The unexpected happening.
292
   * @return false
293
   */
294
  @SuppressWarnings("SameParameterValue")
295
  private boolean alert(
296
      final String titleKey, final String messageKey, final Exception e ) {
297
    final Notifier service = getNotifier();
298
    final Path filePath = getPath();
299
300
    final Notification message = service.createNotification(
301
        get( titleKey ),
302
        get( messageKey ),
303
        filePath == null ? "" : filePath,
304
        e.getMessage()
305
    );
306
307
    try {
308
      service.createError( getWindow(), message ).showAndWait();
309
    } catch( final Exception ex ) {
310
      getNotifier().notify( ex );
311
    }
312
313
    return false;
314
  }
315
316
  private Window getWindow() {
317
    final Scene scene = getEditorPane().getScene();
318
319
    if( scene == null ) {
320
      throw new UnsupportedOperationException( "No scene window available" );
321
    }
322
323
    return scene.getWindow();
324
  }
325
326
  /**
327
   * Returns a best guess at the file encoding. If the encoding could not be
328
   * detected, this will return the default charset for the JVM.
329
   *
330
   * @param bytes The bytes to perform character encoding detection.
331
   * @return The character encoding.
332
   */
333
  private Charset detectEncoding( final byte[] bytes ) {
334
    final var detector = new UniversalDetector( null );
335
    detector.handleData( bytes, 0, bytes.length );
336
    detector.dataEnd();
337
338
    final String charset = detector.getDetectedCharset();
339
340
    return charset == null
341
        ? Charset.defaultCharset()
342
        : Charset.forName( charset.toUpperCase( ENGLISH ) );
343
  }
344
345
  /**
346
   * Converts the given string to an array of bytes using the encoding that was
347
   * originally detected (if any) and associated with this file.
348
   *
349
   * @param text The text to convert into the original file encoding.
350
   * @return A series of bytes ready for writing to a file.
351
   */
352
  private byte[] asBytes( final String text ) {
353
    return text.getBytes( getEncoding() );
354
  }
355
356
  /**
357
   * Converts the given bytes into a Java String. This will call setEncoding
358
   * with the encoding detected by the CharsetDetector.
359
   *
360
   * @param text The text of unknown character encoding.
361
   * @return The text, in its auto-detected encoding, as a String.
362
   */
363
  private String asString( final byte[] text ) {
364
    setEncoding( detectEncoding( text ) );
365
    return new String( text, getEncoding() );
366
  }
367
368
  /**
369
   * Returns the path to the file being edited in this tab.
370
   *
371
   * @return A non-null instance.
372
   */
373
  public Path getPath() {
374
    return mPath;
375
  }
376
377
  /**
378
   * Sets the path to a file for editing and then updates the tab with the
379
   * file contents.
380
   *
381
   * @param path A non-null instance.
382
   */
383
  public void setPath( final Path path ) {
384
    assert path != null;
385
    mPath = path;
386
387
    updateTab();
388
  }
389
390
  public boolean isModified() {
391
    return mModified.get();
392
  }
393
394
  ReadOnlyBooleanProperty modifiedProperty() {
395
    return mModified.getReadOnlyProperty();
396
  }
397
398
  BooleanProperty canUndoProperty() {
399
    return this.canUndo;
400
  }
401
402
  BooleanProperty canRedoProperty() {
403
    return this.canRedo;
404
  }
405
406
  private UndoManager<?> getUndoManager() {
407
    return getEditorPane().getUndoManager();
408
  }
409
410
  /**
411
   * Forwards to the editor pane's listeners for text change events.
412
   *
413
   * @param listener The listener to notify when the text changes.
414
   */
415
  public void addTextChangeListener( final ChangeListener<String> listener ) {
416
    getEditorPane().addTextChangeListener( listener );
417
  }
418
419
  /**
420
   * Forwards to the editor pane's listeners for caret change events.
421
   *
422
   * @param listener Notified when the caret position changes.
423
   */
424
  public void addCaretPositionListener(
425
      final ChangeListener<? super Integer> listener ) {
426
    getEditorPane().addCaretPositionListener( listener );
427
  }
428
429
  /**
430
   * Forwards to the editor pane's listeners for paragraph index change events.
431
   *
432
   * @param listener Notified when the caret's paragraph index changes.
433
   */
434
  public void addCaretParagraphListener(
435
      final ChangeListener<? super Integer> listener ) {
436
    getEditorPane().addCaretParagraphListener( listener );
437
  }
438
439
  public <T extends Event> void addEventFilter(
440
      final EventType<T> eventType,
441
      final EventHandler<? super T> eventFilter ) {
442
    getEditorPane().getEditor().addEventFilter( eventType, eventFilter );
443
  }
444
445
  /**
446
   * Forwards the request to the editor pane.
447
   *
448
   * @return The text to process.
449
   */
450
  public String getEditorText() {
451
    return getEditorPane().getText();
452
  }
453
454
  /**
455
   * Returns the editor pane, or creates one if it doesn't yet exist.
456
   *
457
   * @return The editor pane, never null.
458
   */
459
  @NotNull
460
  public MarkdownEditorPane getEditorPane() {
461
    return mEditorPane;
462
  }
463
464
  /**
465
   * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
466
   * determined.
467
   *
468
   * @return The file encoding or UTF-8 if unknown.
469
   */
470
  private Charset getEncoding() {
471
    return mEncoding;
472
  }
473
474
  private void setEncoding( final Charset encoding ) {
475
    assert encoding != null;
476
    mEncoding = encoding;
477
  }
478
479
  private Notifier getNotifier() {
480
    return mNotifier;
481
  }
482
483
  /**
484
   * Returns the tab title, without any modified indicators.
485
   *
486
   * @return The tab title.
487
   */
488
  @Override
489
  public String toString() {
490
    return getTabTitle();
491
  }
492
}
1493
A src/main/java/com/scrivenvar/FileEditorTabPane.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.predicates.files.FileTypePredicate;
31
import com.scrivenvar.service.Options;
32
import com.scrivenvar.service.Settings;
33
import com.scrivenvar.service.events.Notification;
34
import com.scrivenvar.service.events.Notifier;
35
import com.scrivenvar.util.Utils;
36
import javafx.beans.property.ReadOnlyBooleanProperty;
37
import javafx.beans.property.ReadOnlyBooleanWrapper;
38
import javafx.beans.property.ReadOnlyObjectProperty;
39
import javafx.beans.property.ReadOnlyObjectWrapper;
40
import javafx.beans.value.ChangeListener;
41
import javafx.collections.ListChangeListener;
42
import javafx.collections.ObservableList;
43
import javafx.event.Event;
44
import javafx.scene.Node;
45
import javafx.scene.control.Alert;
46
import javafx.scene.control.ButtonType;
47
import javafx.scene.control.Tab;
48
import javafx.scene.control.TabPane;
49
import javafx.stage.FileChooser;
50
import javafx.stage.FileChooser.ExtensionFilter;
51
import javafx.stage.Window;
52
53
import java.io.File;
54
import java.nio.file.Path;
55
import java.util.ArrayList;
56
import java.util.List;
57
import java.util.Optional;
58
import java.util.concurrent.atomic.AtomicReference;
59
import java.util.prefs.Preferences;
60
import java.util.stream.Collectors;
61
62
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
63
import static com.scrivenvar.FileType.*;
64
import static com.scrivenvar.Messages.get;
65
import static com.scrivenvar.service.events.Notifier.YES;
66
67
/**
68
 * Tab pane for file editors.
69
 */
70
public final class FileEditorTabPane extends TabPane {
71
72
  private final static String FILTER_EXTENSION_TITLES =
73
      "Dialog.file.choose.filter";
74
75
  private final static Options sOptions = Services.load( Options.class );
76
  private final static Settings sSettings = Services.load( Settings.class );
77
  private final static Notifier sNotifier = Services.load( Notifier.class );
78
79
  private final ReadOnlyObjectWrapper<Path> mOpenDefinition =
80
      new ReadOnlyObjectWrapper<>();
81
  private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
82
      new ReadOnlyObjectWrapper<>();
83
  private final ReadOnlyBooleanWrapper mAnyFileEditorModified =
84
      new ReadOnlyBooleanWrapper();
85
  private final ChangeListener<Integer> mCaretPositionListener;
86
  private final ChangeListener<Integer> mCaretParagraphListener;
87
88
  /**
89
   * Constructs a new file editor tab pane.
90
   *
91
   * @param caretPositionListener  Listens for changes to caret position so
92
   *                               that the status bar can update.
93
   * @param caretParagraphListener Listens for changes to the caret's paragraph
94
   *                               so that scrolling may occur.
95
   */
96
  public FileEditorTabPane(
97
      final ChangeListener<Integer> caretPositionListener,
98
      final ChangeListener<Integer> caretParagraphListener ) {
99
    final ObservableList<Tab> tabs = getTabs();
100
101
    setFocusTraversable( false );
102
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
103
104
    addTabSelectionListener(
105
        ( tabPane, oldTab, newTab ) -> {
106
          if( newTab != null ) {
107
            mActiveFileEditor.set( (FileEditorTab) newTab );
108
          }
109
        }
110
    );
111
112
    final ChangeListener<Boolean> modifiedListener =
113
        ( observable, oldValue, newValue ) -> {
114
          for( final Tab tab : tabs ) {
115
            if( ((FileEditorTab) tab).isModified() ) {
116
              mAnyFileEditorModified.set( true );
117
              break;
118
            }
119
          }
120
        };
121
122
    tabs.addListener(
123
        (ListChangeListener<Tab>) change -> {
124
          while( change.next() ) {
125
            if( change.wasAdded() ) {
126
              change.getAddedSubList().forEach(
127
                  ( tab ) -> {
128
                    final var fet = (FileEditorTab) tab;
129
                    fet.modifiedProperty()
130
                       .addListener( modifiedListener );
131
                  } );
132
            }
133
            else if( change.wasRemoved() ) {
134
              change.getRemoved().forEach(
135
                  ( tab ) ->
136
                      ((FileEditorTab) tab).modifiedProperty()
137
                                           .removeListener( modifiedListener ) );
138
            }
139
          }
140
141
          // Changes in the tabs may also change anyFileEditorModified property
142
          // (e.g. closed modified file)
143
          modifiedListener.changed( null, null, null );
144
        }
145
    );
146
147
    mCaretPositionListener = caretPositionListener;
148
    mCaretParagraphListener = caretParagraphListener;
149
  }
150
151
  /**
152
   * Allows observers to be notified when the current file editor tab changes.
153
   *
154
   * @param listener The listener to notify of tab change events.
155
   */
156
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
157
    // Observe the tab so that when a new tab is opened or selected,
158
    // a notification is kicked off.
159
    getSelectionModel().selectedItemProperty().addListener( listener );
160
  }
161
162
  /**
163
   * Returns the tab that has keyboard focus.
164
   *
165
   * @return A non-null instance.
166
   */
167
  public FileEditorTab getActiveFileEditor() {
168
    return mActiveFileEditor.get();
169
  }
170
171
  /**
172
   * Returns the property corresponding to the tab that has focus.
173
   *
174
   * @return A non-null instance.
175
   */
176
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
177
    return mActiveFileEditor.getReadOnlyProperty();
178
  }
179
180
  /**
181
   * Property that can answer whether the text has been modified.
182
   *
183
   * @return A non-null instance, true meaning the content has not been saved.
184
   */
185
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
186
    return mAnyFileEditorModified.getReadOnlyProperty();
187
  }
188
189
  /**
190
   * Creates a new editor instance from the given path.
191
   *
192
   * @param path The file to open.
193
   * @return A non-null instance.
194
   */
195
  private FileEditorTab createFileEditor( final Path path ) {
196
    assert path != null;
197
198
    final FileEditorTab tab = new FileEditorTab( path );
199
200
    tab.setOnCloseRequest( e -> {
201
      if( !canCloseEditor( tab ) ) {
202
        e.consume();
203
      }
204
      else if( isActiveFileEditor( tab ) ) {
205
        // Prevent prompting the user to save when there are no file editor
206
        // tabs open.
207
        mActiveFileEditor.set( null );
208
      }
209
    } );
210
211
    tab.addCaretPositionListener( mCaretPositionListener );
212
    tab.addCaretParagraphListener( mCaretParagraphListener );
213
214
    return tab;
215
  }
216
217
  private boolean isActiveFileEditor( final FileEditorTab tab ) {
218
    return getActiveFileEditor() == tab;
219
  }
220
221
  private Path getDefaultPath() {
222
    final String filename = getDefaultFilename();
223
    return (new File( filename )).toPath();
224
  }
225
226
  private String getDefaultFilename() {
227
    return getSettings().getSetting( "file.default", "untitled.md" );
228
  }
229
230
  /**
231
   * Called to add a new {@link FileEditorTab} to the tab pane.
232
   */
233
  void newEditor() {
234
    final FileEditorTab tab = createFileEditor( getDefaultPath() );
235
236
    getTabs().add( tab );
237
    getSelectionModel().select( tab );
238
  }
239
240
  void openFileDialog() {
241
    final String title = get( "Dialog.file.choose.open.title" );
242
    final FileChooser dialog = createFileChooser( title );
243
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
244
245
    if( files != null ) {
246
      openFiles( files );
247
    }
248
  }
249
250
  /**
251
   * Opens the files into new editors, unless one of those files was a
252
   * definition file. The definition file is loaded into the definition pane,
253
   * but only the first one selected (multiple definition files will result in a
254
   * warning).
255
   *
256
   * @param files The list of non-definition files that the were requested to
257
   *              open.
258
   */
259
  private void openFiles( final List<File> files ) {
260
    final List<String> extensions =
261
        createExtensionFilter( DEFINITION ).getExtensions();
262
    final FileTypePredicate predicate =
263
        new FileTypePredicate( extensions );
264
265
    // The user might have opened multiple definitions files. These will
266
    // be discarded from the text editable files.
267
    final List<File> definitions
268
        = files.stream().filter( predicate ).collect( Collectors.toList() );
269
270
    // Create a modifiable list to remove any definition files that were
271
    // opened.
272
    final List<File> editors = new ArrayList<>( files );
273
274
    if( !editors.isEmpty() ) {
275
      saveLastDirectory( editors.get( 0 ) );
276
    }
277
278
    editors.removeAll( definitions );
279
280
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
281
    if( !editors.isEmpty() ) {
282
      openEditors( editors, 0 );
283
    }
284
285
    if( !definitions.isEmpty() ) {
286
      openDefinition( definitions.get( 0 ) );
287
    }
288
  }
289
290
  private void openEditors( final List<File> files, final int activeIndex ) {
291
    final int fileTally = files.size();
292
    final List<Tab> tabs = getTabs();
293
294
    // Close single unmodified "Untitled" tab.
295
    if( tabs.size() == 1 ) {
296
      final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
297
298
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
299
        closeEditor( fileEditor, false );
300
      }
301
    }
302
303
    for( int i = 0; i < fileTally; i++ ) {
304
      final Path path = files.get( i ).toPath();
305
306
      FileEditorTab fileEditorTab = findEditor( path );
307
308
      // Only open new files.
309
      if( fileEditorTab == null ) {
310
        fileEditorTab = createFileEditor( path );
311
        getTabs().add( fileEditorTab );
312
      }
313
314
      // Select the first file in the list.
315
      if( i == activeIndex ) {
316
        getSelectionModel().select( fileEditorTab );
317
      }
318
    }
319
  }
320
321
  /**
322
   * Returns a property that changes when a new definition file is opened.
323
   *
324
   * @return The path to a definition file that was opened.
325
   */
326
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
327
    return getOnOpenDefinitionFile().getReadOnlyProperty();
328
  }
329
330
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
331
    return mOpenDefinition;
332
  }
333
334
  /**
335
   * Called when the user has opened a definition file (using the file open
336
   * dialog box). This will replace the current set of definitions for the
337
   * active tab.
338
   *
339
   * @param definition The file to open.
340
   */
341
  private void openDefinition( final File definition ) {
342
    // TODO: Prevent reading this file twice when a new text document is opened.
343
    // (might be a matter of checking the value first).
344
    getOnOpenDefinitionFile().set( definition.toPath() );
345
  }
346
347
  /**
348
   * Called when the contents of the editor are to be saved.
349
   *
350
   * @param tab The tab containing content to save.
351
   * @return true The contents were saved (or needn't be saved).
352
   */
353
  public boolean saveEditor( final FileEditorTab tab ) {
354
    if( tab == null || !tab.isModified() ) {
355
      return true;
356
    }
357
358
    return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
359
  }
360
361
  /**
362
   * Opens the Save As dialog for the user to save the content under a new
363
   * path.
364
   *
365
   * @param tab The tab with contents to save.
366
   * @return true The contents were saved, or the tab was null.
367
   */
368
  public boolean saveEditorAs( final FileEditorTab tab ) {
369
    if( tab == null ) {
370
      return true;
371
    }
372
373
    getSelectionModel().select( tab );
374
375
    final FileChooser fileChooser = createFileChooser( get(
376
        "Dialog.file.choose.save.title" ) );
377
    final File file = fileChooser.showSaveDialog( getWindow() );
378
    if( file == null ) {
379
      return false;
380
    }
381
382
    saveLastDirectory( file );
383
    tab.setPath( file.toPath() );
384
385
    return tab.save();
386
  }
387
388
  void saveAllEditors() {
389
    for( final FileEditorTab fileEditor : getAllEditors() ) {
390
      saveEditor( fileEditor );
391
    }
392
  }
393
394
  /**
395
   * Answers whether the file has had modifications. '
396
   *
397
   * @param tab THe tab to check for modifications.
398
   * @return false The file is unmodified.
399
   */
400
  @SuppressWarnings("BooleanMethodIsAlwaysInverted")
401
  boolean canCloseEditor( final FileEditorTab tab ) {
402
    final AtomicReference<Boolean> canClose = new AtomicReference<>();
403
    canClose.set( true );
404
405
    if( tab.isModified() ) {
406
      final Notification message = getNotifyService().createNotification(
407
          Messages.get( "Alert.file.close.title" ),
408
          Messages.get( "Alert.file.close.text" ),
409
          tab.getText()
410
      );
411
412
      final Alert confirmSave = getNotifyService().createConfirmation(
413
          getWindow(), message );
414
415
      final Optional<ButtonType> buttonType = confirmSave.showAndWait();
416
417
      buttonType.ifPresent(
418
          save -> canClose.set(
419
              save == YES ? saveEditor( tab ) : save == ButtonType.NO
420
          )
421
      );
422
    }
423
424
    return canClose.get();
425
  }
426
427
  boolean closeEditor( final FileEditorTab tab, final boolean save ) {
428
    if( tab == null ) {
429
      return true;
430
    }
431
432
    if( save ) {
433
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
434
      Event.fireEvent( tab, event );
435
436
      if( event.isConsumed() ) {
437
        return false;
438
      }
439
    }
440
441
    getTabs().remove( tab );
442
443
    if( tab.getOnClosed() != null ) {
444
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
445
    }
446
447
    return true;
448
  }
449
450
  boolean closeAllEditors() {
451
    final FileEditorTab[] allEditors = getAllEditors();
452
    final FileEditorTab activeEditor = getActiveFileEditor();
453
454
    // try to save active tab first because in case the user decides to cancel,
455
    // then it stays active
456
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
457
      return false;
458
    }
459
460
    // This should be called any time a tab changes.
461
    persistPreferences();
462
463
    // save modified tabs
464
    for( int i = 0; i < allEditors.length; i++ ) {
465
      final FileEditorTab fileEditor = allEditors[ i ];
466
467
      if( fileEditor == activeEditor ) {
468
        continue;
469
      }
470
471
      if( fileEditor.isModified() ) {
472
        // activate the modified tab to make its modified content visible to
473
        // the user
474
        getSelectionModel().select( i );
475
476
        if( !canCloseEditor( fileEditor ) ) {
477
          return false;
478
        }
479
      }
480
    }
481
482
    // Close all tabs.
483
    for( final FileEditorTab fileEditor : allEditors ) {
484
      if( !closeEditor( fileEditor, false ) ) {
485
        return false;
486
      }
487
    }
488
489
    return getTabs().isEmpty();
490
  }
491
492
  private FileEditorTab[] getAllEditors() {
493
    final ObservableList<Tab> tabs = getTabs();
494
    final int length = tabs.size();
495
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
496
497
    for( int i = 0; i < length; i++ ) {
498
      allEditors[ i ] = (FileEditorTab) tabs.get( i );
499
    }
500
501
    return allEditors;
502
  }
503
504
  /**
505
   * Returns the file editor tab that has the given path.
506
   *
507
   * @return null No file editor tab for the given path was found.
508
   */
509
  private FileEditorTab findEditor( final Path path ) {
510
    for( final Tab tab : getTabs() ) {
511
      final FileEditorTab fileEditor = (FileEditorTab) tab;
512
513
      if( fileEditor.isPath( path ) ) {
514
        return fileEditor;
515
      }
516
    }
517
518
    return null;
519
  }
520
521
  private FileChooser createFileChooser( String title ) {
522
    final FileChooser fileChooser = new FileChooser();
523
524
    fileChooser.setTitle( title );
525
    fileChooser.getExtensionFilters().addAll(
526
        createExtensionFilters() );
527
528
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
529
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
530
531
    if( !file.isDirectory() ) {
532
      file = new File( "." );
533
    }
534
535
    fileChooser.setInitialDirectory( file );
536
    return fileChooser;
537
  }
538
539
  private List<ExtensionFilter> createExtensionFilters() {
540
    final List<ExtensionFilter> list = new ArrayList<>();
541
542
    // TODO: Return a list of all properties that match the filter prefix.
543
    // This will allow dynamic filters to be added and removed just by
544
    // updating the properties file.
545
    list.add( createExtensionFilter( ALL ) );
546
    list.add( createExtensionFilter( SOURCE ) );
547
    list.add( createExtensionFilter( DEFINITION ) );
548
    list.add( createExtensionFilter( XML ) );
549
    return list;
550
  }
551
552
  /**
553
   * Returns a filter for file name extensions recognized by the application
554
   * that can be opened by the user.
555
   *
556
   * @param filetype Used to find the globbing pattern for extensions.
557
   * @return A filename filter suitable for use by a FileDialog instance.
558
   */
559
  private ExtensionFilter createExtensionFilter( final FileType filetype ) {
560
    final String tKey = String.format( "%s.title.%s",
561
                                       FILTER_EXTENSION_TITLES,
562
                                       filetype );
563
    final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
564
565
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
566
  }
567
568
  private void saveLastDirectory( final File file ) {
569
    getPreferences().put( "lastDirectory", file.getParent() );
570
  }
571
572
  public void initPreferences() {
573
    int activeIndex = 0;
574
575
    final Preferences preferences = getPreferences();
576
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
577
    final String activeFileName = preferences.get( "activeFile", null );
578
579
    final List<File> files = new ArrayList<>( fileNames.length );
580
581
    for( final String fileName : fileNames ) {
582
      final File file = new File( fileName );
583
584
      if( file.exists() ) {
585
        files.add( file );
586
587
        if( fileName.equals( activeFileName ) ) {
588
          activeIndex = files.size() - 1;
589
        }
590
      }
591
    }
592
593
    if( files.isEmpty() ) {
594
      newEditor();
595
    }
596
    else {
597
      openEditors( files, activeIndex );
598
    }
599
  }
600
601
  public void persistPreferences() {
602
    final ObservableList<Tab> allEditors = getTabs();
603
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
604
605
    for( final Tab tab : allEditors ) {
606
      final FileEditorTab fileEditor = (FileEditorTab) tab;
607
      final Path filePath = fileEditor.getPath();
608
609
      if( filePath != null ) {
610
        fileNames.add( filePath.toString() );
611
      }
612
    }
613
614
    final Preferences preferences = getPreferences();
615
    Utils.putPrefsStrings( preferences,
616
                           "file",
617
                           fileNames.toArray( new String[ 0 ] ) );
618
619
    final FileEditorTab activeEditor = getActiveFileEditor();
620
    final Path filePath = activeEditor == null ? null : activeEditor.getPath();
621
622
    if( filePath == null ) {
623
      preferences.remove( "activeFile" );
624
    }
625
    else {
626
      preferences.put( "activeFile", filePath.toString() );
627
    }
628
  }
629
630
  private List<String> getExtensions( final String key ) {
631
    return getSettings().getStringSettingList( key );
632
  }
633
634
  private Notifier getNotifyService() {
635
    return sNotifier;
636
  }
637
638
  private Settings getSettings() {
639
    return sSettings;
640
  }
641
642
  protected Options getOptions() {
643
    return sOptions;
644
  }
645
646
  private Window getWindow() {
647
    return getScene().getWindow();
648
  }
649
650
  private Preferences getPreferences() {
651
    return getOptions().getState();
652
  }
653
654
  Node getNode() {
655
    return this;
656
  }
657
}
1658
A src/main/java/com/scrivenvar/FileType.java
1
/*
2
 * Copyright 2020 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 {@code
33
 * settings.properties}.
34
 */
35
public enum FileType {
36
37
  ALL( "all" ),
38
  RMARKDOWN( "rmarkdown" ),
39
  RXML( "rxml" ),
40
  SOURCE( "source" ),
41
  DEFINITION( "definition" ),
42
  XML( "xml" ),
43
  CSV( "csv" ),
44
  JSON( "json" ),
45
  TOML( "toml" ),
46
  YAML( "yaml" ),
47
  PROPERTIES( "properties" ),
48
  UNKNOWN( "unknown" );
49
50
  private final String mType;
51
52
  /**
53
   * Default constructor for enumerated file type.
54
   *
55
   * @param type Human-readable name for the file type.
56
   */
57
  FileType( final String type ) {
58
    mType = type;
59
  }
60
61
  /**
62
   * Returns the file type that corresponds to the given string.
63
   *
64
   * @param type The string to compare against this enumeration of file types.
65
   * @return The corresponding File Type for the given string.
66
   * @throws IllegalArgumentException Type not found.
67
   */
68
  public static FileType from( final String type ) {
69
    for( final FileType fileType : FileType.values() ) {
70
      if( fileType.isType( type ) ) {
71
        return fileType;
72
      }
73
    }
74
75
    throw new IllegalArgumentException( type );
76
  }
77
78
  /**
79
   * Answers whether this file type matches the given string, case insensitive
80
   * comparison.
81
   *
82
   * @param type Presumably a file name extension to check against.
83
   * @return true The given extension corresponds to this enumerated type.
84
   */
85
  public boolean isType( final String type ) {
86
    return getType().equalsIgnoreCase( type );
87
  }
88
89
  /**
90
   * Returns the human-readable name for the file type.
91
   *
92
   * @return A non-null instance.
93
   */
94
  private String getType() {
95
    return mType;
96
  }
97
98
  /**
99
   * Returns the lowercase version of the file name extension.
100
   *
101
   * @return The file name, in lower case.
102
   */
103
  @Override
104
  public String toString() {
105
    return getType();
106
  }
107
}
1108
A src/main/java/com/scrivenvar/Launcher.java
1
/*
2
 * Copyright 2020 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.io.IOException;
31
import java.io.InputStream;
32
import java.util.Calendar;
33
import java.util.Properties;
34
35
import static java.lang.String.format;
36
37
/**
38
 * Launches the application using the {@link Main} class.
39
 *
40
 * <p>
41
 * This is required until modules are implemented, which may never happen
42
 * because the application should be ported away from Java and JavaFX.
43
 * </p>
44
 */
45
public class Launcher {
46
  /**
47
   * Delegates to the application entry point.
48
   *
49
   * @param args Command-line arguments.
50
   */
51
  public static void main( final String[] args ) throws IOException {
52
    showAppInfo();
53
    Main.main( args );
54
  }
55
56
  @SuppressWarnings("RedundantStringFormatCall")
57
  private static void showAppInfo() throws IOException {
58
    out( format( "%s version %s", getTitle(), getVersion() ) );
59
    out( format( "Copyright %s White Magic Software, Ltd.", getYear() ) );
60
    out( format( "Portions copyright 2020 Karl Tauber." ) );
61
  }
62
63
  private static void out( final String s ) {
64
    System.out.println( s );
65
  }
66
67
  private static String getTitle() throws IOException {
68
    final Properties properties = loadProperties( "messages.properties" );
69
    return properties.getProperty( "Main.title" );
70
  }
71
72
  private static String getVersion() throws IOException {
73
    final Properties properties = loadProperties( "app.properties" );
74
    return properties.getProperty( "application.version" );
75
  }
76
77
  private static String getYear() {
78
    return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) );
79
  }
80
81
  @SuppressWarnings("SameParameterValue")
82
  private static Properties loadProperties( final String resource )
83
      throws IOException {
84
    final Properties properties = new Properties();
85
    properties.load( getResourceAsStream( getResourceName( resource ) ) );
86
    return properties;
87
  }
88
89
  private static String getResourceName( final String resource ) {
90
    return format( "%s/%s", getPackagePath(), resource );
91
  }
92
93
  private static String getPackagePath() {
94
    return Launcher.class.getPackageName().replace( '.', '/' );
95
  }
96
97
  private static InputStream getResourceAsStream( final String resource ) {
98
    return Launcher.class.getClassLoader().getResourceAsStream( resource );
99
  }
100
}
1101
A src/main/java/com/scrivenvar/Main.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.preferences.FilePreferencesFactory;
31
import com.scrivenvar.service.Options;
32
import com.scrivenvar.service.Snitch;
33
import com.scrivenvar.service.events.Notifier;
34
import com.scrivenvar.util.ResourceWalker;
35
import com.scrivenvar.util.StageState;
36
import javafx.application.Application;
37
import javafx.scene.Scene;
38
import javafx.scene.image.Image;
39
import javafx.stage.Stage;
40
41
import java.awt.*;
42
import java.io.FileInputStream;
43
import java.io.IOException;
44
import java.io.InputStream;
45
import java.net.URI;
46
import java.util.Map;
47
import java.util.logging.LogManager;
48
49
import static com.scrivenvar.Constants.*;
50
import static com.scrivenvar.Messages.get;
51
import static java.awt.font.TextAttribute.LIGATURES;
52
import static java.awt.font.TextAttribute.LIGATURES_ON;
53
import static javafx.scene.input.KeyCode.F11;
54
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
55
56
/**
57
 * Application entry point. The application allows users to edit Markdown
58
 * files and see a real-time preview of the edits.
59
 */
60
public final class Main extends Application {
61
62
  static {
63
    // Suppress logging to standard output.
64
    LogManager.getLogManager().reset();
65
66
    // Suppress logging to standard error.
67
    System.err.close();
68
  }
69
70
  /**
71
   * Must be static, otherwise instant crash.
72
   */
73
  private final static Notifier sNotifier = Services.load( Notifier.class );
74
  private final Options mOptions = Services.load( Options.class );
75
  private final Snitch mSnitch = Services.load( Snitch.class );
76
77
  private final Thread mSnitchThread = new Thread( getSnitch() );
78
  private final MainWindow mMainWindow = new MainWindow();
79
80
  @SuppressWarnings({"FieldCanBeLocal", "unused"})
81
  private StageState mStageState;
82
83
  /**
84
   * Application entry point.
85
   *
86
   * @param args Command-line arguments.
87
   */
88
  public static void main( final String[] args ) {
89
    initPreferences();
90
    initFonts();
91
    launch( args );
92
  }
93
94
  /**
95
   * JavaFX entry point.
96
   *
97
   * @param stage The primary application stage.
98
   */
99
  @Override
100
  public void start( final Stage stage ) {
101
    initState( stage );
102
    initStage( stage );
103
    initSnitch();
104
105
    stage.show();
106
107
    // After the stage is visible, the panel dimensions are known, which
108
    // allows scaling images to fit the preview panel.
109
    getMainWindow().init();
110
  }
111
112
  /**
113
   * This needs to run before the windowing system kicks in, otherwise the
114
   * fonts will not be found.
115
   */
116
  @SuppressWarnings({"rawtypes", "unchecked"})
117
  private static void initFonts() {
118
    final var ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
119
120
    try {
121
      ResourceWalker.walk(
122
          FONT_DIRECTORY, path -> {
123
            final var uri = path.toUri();
124
            final var filename = path.toString();
125
126
            try( final var is = openFont( uri, filename ) ) {
127
              final var font = Font.createFont( Font.TRUETYPE_FONT, is );
128
              final Map attributes = font.getAttributes();
129
130
              attributes.put( LIGATURES, LIGATURES_ON );
131
              ge.registerFont( font.deriveFont( attributes ) );
132
            } catch( final Exception e ) {
133
              getNotifier().notify( e );
134
            }
135
          }
136
      );
137
    } catch( final Exception e ) {
138
      getNotifier().notify( e );
139
    }
140
  }
141
142
  private static InputStream openFont( final URI uri, final String filename )
143
      throws IOException {
144
    return uri.getScheme().equals( "jar" )
145
        ? Main.class.getResourceAsStream( filename )
146
        : new FileInputStream( filename );
147
  }
148
149
  /**
150
   * Sets the factory used for reading user preferences.
151
   */
152
  private static void initPreferences() {
153
    System.setProperty(
154
        "java.util.prefs.PreferencesFactory",
155
        FilePreferencesFactory.class.getName()
156
    );
157
  }
158
159
  private void initState( final Stage stage ) {
160
    mStageState = new StageState( stage, getOptions().getState() );
161
  }
162
163
  private void initStage( final Stage stage ) {
164
    stage.getIcons().addAll(
165
        createImage( FILE_LOGO_16 ),
166
        createImage( FILE_LOGO_32 ),
167
        createImage( FILE_LOGO_128 ),
168
        createImage( FILE_LOGO_256 ),
169
        createImage( FILE_LOGO_512 ) );
170
    stage.setTitle( getApplicationTitle() );
171
    stage.setScene( getScene() );
172
173
    stage.addEventHandler( KEY_PRESSED, event -> {
174
      if( F11.equals( event.getCode() ) ) {
175
        stage.setFullScreen( !stage.isFullScreen() );
176
      }
177
    } );
178
  }
179
180
  /**
181
   * Watch for file system changes.
182
   */
183
  private void initSnitch() {
184
    getSnitchThread().start();
185
  }
186
187
  /**
188
   * Stops the snitch service, if its running.
189
   *
190
   * @throws InterruptedException Couldn't stop the snitch thread.
191
   */
192
  @Override
193
  public void stop() throws InterruptedException {
194
    getSnitch().stop();
195
196
    final Thread thread = getSnitchThread();
197
    thread.interrupt();
198
    thread.join();
199
  }
200
201
  private static Notifier getNotifier() {
202
    return sNotifier;
203
  }
204
205
  private Snitch getSnitch() {
206
    return mSnitch;
207
  }
208
209
  private Thread getSnitchThread() {
210
    return mSnitchThread;
211
  }
212
213
  private Options getOptions() {
214
    return mOptions;
215
  }
216
217
  private MainWindow getMainWindow() {
218
    return mMainWindow;
219
  }
220
221
  private Scene getScene() {
222
    return getMainWindow().getScene();
223
  }
224
225
  private String getApplicationTitle() {
226
    return get( "Main.title" );
227
  }
228
229
  private Image createImage( final String filename ) {
230
    return new Image( filename );
231
  }
232
}
1233
A src/main/java/com/scrivenvar/MainWindow.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.definition.DefinitionFactory;
31
import com.scrivenvar.definition.DefinitionPane;
32
import com.scrivenvar.definition.DefinitionSource;
33
import com.scrivenvar.definition.MapInterpolator;
34
import com.scrivenvar.definition.yaml.YamlDefinitionSource;
35
import com.scrivenvar.editors.EditorPane;
36
import com.scrivenvar.editors.VariableNameInjector;
37
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
38
import com.scrivenvar.preferences.UserPreferences;
39
import com.scrivenvar.preview.HTMLPreviewPane;
40
import com.scrivenvar.processors.HtmlPreviewProcessor;
41
import com.scrivenvar.processors.Processor;
42
import com.scrivenvar.processors.ProcessorFactory;
43
import com.scrivenvar.service.Options;
44
import com.scrivenvar.service.Snitch;
45
import com.scrivenvar.service.events.Notifier;
46
import com.scrivenvar.spelling.api.SpellCheckListener;
47
import com.scrivenvar.spelling.api.SpellChecker;
48
import com.scrivenvar.spelling.impl.PermissiveSpeller;
49
import com.scrivenvar.spelling.impl.SymSpellSpeller;
50
import com.scrivenvar.util.Action;
51
import com.scrivenvar.util.ActionBuilder;
52
import com.scrivenvar.util.ActionUtils;
53
import com.vladsch.flexmark.parser.Parser;
54
import com.vladsch.flexmark.util.ast.NodeVisitor;
55
import com.vladsch.flexmark.util.ast.VisitHandler;
56
import javafx.beans.binding.Bindings;
57
import javafx.beans.binding.BooleanBinding;
58
import javafx.beans.property.BooleanProperty;
59
import javafx.beans.property.SimpleBooleanProperty;
60
import javafx.beans.value.ChangeListener;
61
import javafx.beans.value.ObservableBooleanValue;
62
import javafx.beans.value.ObservableValue;
63
import javafx.collections.ListChangeListener.Change;
64
import javafx.collections.ObservableList;
65
import javafx.event.Event;
66
import javafx.event.EventHandler;
67
import javafx.geometry.Pos;
68
import javafx.scene.Node;
69
import javafx.scene.Scene;
70
import javafx.scene.control.*;
71
import javafx.scene.control.Alert.AlertType;
72
import javafx.scene.image.Image;
73
import javafx.scene.image.ImageView;
74
import javafx.scene.input.Clipboard;
75
import javafx.scene.input.ClipboardContent;
76
import javafx.scene.input.KeyEvent;
77
import javafx.scene.layout.BorderPane;
78
import javafx.scene.layout.VBox;
79
import javafx.scene.text.Text;
80
import javafx.stage.Window;
81
import javafx.stage.WindowEvent;
82
import javafx.util.Duration;
83
import org.apache.commons.lang3.SystemUtils;
84
import org.controlsfx.control.StatusBar;
85
import org.fxmisc.richtext.StyleClassedTextArea;
86
import org.fxmisc.richtext.model.StyleSpansBuilder;
87
import org.reactfx.value.Val;
88
89
import java.io.BufferedReader;
90
import java.io.InputStreamReader;
91
import java.nio.file.Path;
92
import java.nio.file.Paths;
93
import java.util.*;
94
import java.util.concurrent.atomic.AtomicInteger;
95
import java.util.function.Consumer;
96
import java.util.function.Function;
97
import java.util.prefs.Preferences;
98
import java.util.stream.Collectors;
99
100
import static com.scrivenvar.Constants.*;
101
import static com.scrivenvar.Messages.get;
102
import static com.scrivenvar.util.StageState.*;
103
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
104
import static java.nio.charset.StandardCharsets.UTF_8;
105
import static java.util.Collections.emptyList;
106
import static java.util.Collections.singleton;
107
import static javafx.application.Platform.runLater;
108
import static javafx.event.Event.fireEvent;
109
import static javafx.scene.input.KeyCode.ENTER;
110
import static javafx.scene.input.KeyCode.TAB;
111
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
112
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
113
114
/**
115
 * Main window containing a tab pane in the center for file editors.
116
 */
117
public class MainWindow implements Observer {
118
  /**
119
   * The {@code OPTIONS} variable must be declared before all other variables
120
   * to prevent subsequent initializations from failing due to missing user
121
   * preferences.
122
   */
123
  private final static Options sOptions = Services.load( Options.class );
124
  private final static Snitch SNITCH = Services.load( Snitch.class );
125
  private final static Notifier sNotifier = Services.load( Notifier.class );
126
127
  private final Scene mScene;
128
  private final StatusBar mStatusBar;
129
  private final Text mLineNumberText;
130
  private final TextField mFindTextField;
131
  private final SpellChecker mSpellChecker;
132
133
  private final Object mMutex = new Object();
134
135
  /**
136
   * Prevents re-instantiation of processing classes.
137
   */
138
  private final Map<FileEditorTab, Processor<String>> mProcessors =
139
      new HashMap<>();
140
141
  private final Map<String, String> mResolvedMap =
142
      new HashMap<>( DEFAULT_MAP_SIZE );
143
144
  /**
145
   * Called when the definition data is changed.
146
   */
147
  private final EventHandler<TreeItem.TreeModificationEvent<Event>>
148
      mTreeHandler = event -> {
149
    exportDefinitions( getDefinitionPath() );
150
    interpolateResolvedMap();
151
    renderActiveTab();
152
  };
153
154
  /**
155
   * Called to switch to the definition pane when the user presses the TAB key.
156
   */
157
  private final EventHandler<? super KeyEvent> mTabKeyHandler =
158
      (EventHandler<KeyEvent>) event -> {
159
        if( event.getCode() == TAB ) {
160
          getDefinitionPane().requestFocus();
161
          event.consume();
162
        }
163
      };
164
165
  /**
166
   * Called to inject the selected item when the user presses ENTER in the
167
   * definition pane.
168
   */
169
  private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
170
      event -> {
171
        if( event.getCode() == ENTER ) {
172
          getVariableNameInjector().injectSelectedItem();
173
        }
174
      };
175
176
  private final ChangeListener<Integer> mCaretPositionListener =
177
      ( observable, oldPosition, newPosition ) -> {
178
        final FileEditorTab tab = getActiveFileEditorTab();
179
        final EditorPane pane = tab.getEditorPane();
180
        final StyleClassedTextArea editor = pane.getEditor();
181
182
        getLineNumberText().setText(
183
            get( STATUS_BAR_LINE,
184
                 editor.getCurrentParagraph() + 1,
185
                 editor.getParagraphs().size(),
186
                 editor.getCaretPosition()
187
            )
188
        );
189
      };
190
191
  private final ChangeListener<Integer> mCaretParagraphListener =
192
      ( observable, oldIndex, newIndex ) ->
193
          scrollToParagraph( newIndex, true );
194
195
  private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
196
  private final DefinitionPane mDefinitionPane = new DefinitionPane();
197
  private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
198
  private final FileEditorTabPane mFileEditorPane = new FileEditorTabPane(
199
      mCaretPositionListener,
200
      mCaretParagraphListener );
201
202
  /**
203
   * Listens on the definition pane for double-click events.
204
   */
205
  private final VariableNameInjector mVariableNameInjector
206
      = new VariableNameInjector( mDefinitionPane );
207
208
  public MainWindow() {
209
    sNotifier.addObserver( this );
210
211
    mStatusBar = createStatusBar();
212
    mLineNumberText = createLineNumberText();
213
    mFindTextField = createFindTextField();
214
    mScene = createScene();
215
    mSpellChecker = createSpellChecker();
216
217
    // Add the close request listener before the window is shown.
218
    initLayout();
219
  }
220
221
  /**
222
   * Called after the stage is shown.
223
   */
224
  public void init() {
225
    initFindInput();
226
    initSnitch();
227
    initDefinitionListener();
228
    initTabAddedListener();
229
    initTabChangedListener();
230
    initPreferences();
231
    initVariableNameInjector();
232
  }
233
234
  private void initLayout() {
235
    final var appScene = getScene();
236
237
    appScene.getStylesheets().add( STYLESHEET_SCENE );
238
    appScene.windowProperty().addListener(
239
        ( unused, oldWindow, newWindow ) ->
240
            newWindow.setOnCloseRequest(
241
                e -> {
242
                  if( !getFileEditorPane().closeAllEditors() ) {
243
                    e.consume();
244
                  }
245
                }
246
            )
247
    );
248
  }
249
250
  /**
251
   * Initialize the find input text field to listen on F3, ENTER, and
252
   * ESCAPE key presses.
253
   */
254
  private void initFindInput() {
255
    final TextField input = getFindTextField();
256
257
    input.setOnKeyPressed( ( KeyEvent event ) -> {
258
      switch( event.getCode() ) {
259
        case F3:
260
        case ENTER:
261
          editFindNext();
262
          break;
263
        case F:
264
          if( !event.isControlDown() ) {
265
            break;
266
          }
267
        case ESCAPE:
268
          getStatusBar().setGraphic( null );
269
          getActiveFileEditorTab().getEditorPane().requestFocus();
270
          break;
271
      }
272
    } );
273
274
    // Remove when the input field loses focus.
275
    input.focusedProperty().addListener(
276
        ( focused, oldFocus, newFocus ) -> {
277
          if( !newFocus ) {
278
            getStatusBar().setGraphic( null );
279
          }
280
        }
281
    );
282
  }
283
284
  /**
285
   * Watch for changes to external files. In particular, this awaits
286
   * modifications to any XSL files associated with XML files being edited.
287
   * When
288
   * an XSL file is modified (external to the application), the snitch's ears
289
   * perk up and the file is reloaded. This keeps the XSL transformation up to
290
   * date with what's on the file system.
291
   */
292
  private void initSnitch() {
293
    SNITCH.addObserver( this );
294
  }
295
296
  /**
297
   * Listen for {@link FileEditorTabPane} to receive open definition file
298
   * event.
299
   */
300
  private void initDefinitionListener() {
301
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
302
        ( final ObservableValue<? extends Path> file,
303
          final Path oldPath, final Path newPath ) -> {
304
          // Indirectly refresh the resolved map.
305
          resetProcessors();
306
307
          openDefinitions( newPath );
308
309
          // Will create new processors and therefore a new resolved map.
310
          renderActiveTab();
311
        }
312
    );
313
  }
314
315
  /**
316
   * When tabs are added, hook the various change listeners onto the new
317
   * tab sothat the preview pane refreshes as necessary.
318
   */
319
  private void initTabAddedListener() {
320
    final FileEditorTabPane editorPane = getFileEditorPane();
321
322
    // Make sure the text processor kicks off when new files are opened.
323
    final ObservableList<Tab> tabs = editorPane.getTabs();
324
325
    // Update the preview pane on tab changes.
326
    tabs.addListener(
327
        ( final Change<? extends Tab> change ) -> {
328
          while( change.next() ) {
329
            if( change.wasAdded() ) {
330
              // Multiple tabs can be added simultaneously.
331
              for( final Tab newTab : change.getAddedSubList() ) {
332
                final FileEditorTab tab = (FileEditorTab) newTab;
333
334
                initTextChangeListener( tab );
335
                initTabKeyEventListener( tab );
336
                initScrollEventListener( tab );
337
                initSpellCheckListener( tab );
338
//              initSyntaxListener( tab );
339
              }
340
            }
341
          }
342
        }
343
    );
344
  }
345
346
  private void initTextChangeListener( final FileEditorTab tab ) {
347
    tab.addTextChangeListener(
348
        ( editor, oldValue, newValue ) -> {
349
          process( tab );
350
          scrollToParagraph( getCurrentParagraphIndex() );
351
        }
352
    );
353
  }
354
355
  /**
356
   * Ensure that the keyboard events are received when a new tab is added
357
   * to the user interface.
358
   *
359
   * @param tab The tab editor that can trigger keyboard events.
360
   */
361
  private void initTabKeyEventListener( final FileEditorTab tab ) {
362
    tab.addEventFilter( KeyEvent.KEY_PRESSED, mTabKeyHandler );
363
  }
364
365
  private void initScrollEventListener( final FileEditorTab tab ) {
366
    final var scrollPane = tab.getScrollPane();
367
    final var scrollBar = getPreviewPane().getVerticalScrollBar();
368
369
    addShowListener( scrollPane, ( __ ) -> {
370
      final var handler = new ScrollEventHandler( scrollPane, scrollBar );
371
      handler.enabledProperty().bind( tab.selectedProperty() );
372
    } );
373
  }
374
375
  /**
376
   * Listen for changes to the any particular paragraph and perform a quick
377
   * spell check upon it. The style classes in the editor will be changed to
378
   * mark any spelling mistakes in the paragraph. The user may then interact
379
   * with any misspelled word (i.e., any piece of text that is marked) to
380
   * revise the spelling.
381
   *
382
   * @param tab The tab to spellcheck.
383
   */
384
  private void initSpellCheckListener( final FileEditorTab tab ) {
385
    final var editor = tab.getEditorPane().getEditor();
386
387
    // When the editor first appears, run a full spell check. This allows
388
    // spell checking while typing to be restricted to the active paragraph,
389
    // which is usually substantially smaller than the whole document.
390
    addShowListener(
391
        editor, ( __ ) -> spellcheck( editor, editor.getText() )
392
    );
393
394
    // Use the plain text changes so that notifications of style changes
395
    // are suppressed. Checking against the identity ensures that only
396
    // new text additions or deletions trigger proofreading.
397
    editor.plainTextChanges()
398
          .filter( p -> !p.isIdentity() ).subscribe( change -> {
399
400
      // Only perform a spell check on the current paragraph. The
401
      // entire document is processed once, when opened.
402
      final var offset = change.getPosition();
403
      final var position = editor.offsetToPosition( offset, Forward );
404
      final var paraId = position.getMajor();
405
      final var paragraph = editor.getParagraph( paraId );
406
      final var text = paragraph.getText();
407
408
      // Ensure that styles aren't doubled-up.
409
      editor.clearStyle( paraId );
410
411
      spellcheck( editor, text, paraId );
412
    } );
413
  }
414
415
  /**
416
   * Listen for new tab selection events.
417
   */
418
  private void initTabChangedListener() {
419
    final FileEditorTabPane editorPane = getFileEditorPane();
420
421
    // Update the preview pane changing tabs.
422
    editorPane.addTabSelectionListener(
423
        ( tabPane, oldTab, newTab ) -> {
424
          if( newTab == null ) {
425
            // Clear the preview pane when closing an editor. When the last
426
            // tab is closed, this ensures that the preview pane is empty.
427
            getPreviewPane().clear();
428
          }
429
          else {
430
            final var tab = (FileEditorTab) newTab;
431
            updateVariableNameInjector( tab );
432
            process( tab );
433
          }
434
        }
435
    );
436
  }
437
438
  /**
439
   * Reloads the preferences from the previous session.
440
   */
441
  private void initPreferences() {
442
    initDefinitionPane();
443
    getFileEditorPane().initPreferences();
444
  }
445
446
  private void initVariableNameInjector() {
447
    updateVariableNameInjector( getActiveFileEditorTab() );
448
  }
449
450
  /**
451
   * Calls the listener when the given node is shown for the first time. The
452
   * visible property is not the same as the initial showing event; visibility
453
   * can be triggered numerous times (such as going off screen).
454
   * <p>
455
   * This is called, for example, before the drag handler can be attached,
456
   * because the scrollbar for the text editor pane must be visible.
457
   * </p>
458
   *
459
   * @param node     The node to watch for showing.
460
   * @param consumer The consumer to invoke when the event fires.
461
   */
462
  private void addShowListener(
463
      final Node node, final Consumer<Void> consumer ) {
464
    final ChangeListener<? super Boolean> listener = ( o, oldShow, newShow ) ->
465
        runLater( () -> {
466
          if( newShow ) {
467
            try {
468
              consumer.accept( null );
469
            } catch( final Exception ex ) {
470
              error( ex );
471
            }
472
          }
473
        } );
474
475
    Val.flatMap( node.sceneProperty(), Scene::windowProperty )
476
       .flatMap( Window::showingProperty )
477
       .addListener( listener );
478
  }
479
480
  private void scrollToParagraph( final int id ) {
481
    scrollToParagraph( id, false );
482
  }
483
484
  /**
485
   * @param id    The paragraph to scroll to, will be approximated if it doesn't
486
   *              exist.
487
   * @param force {@code true} means to force scrolling immediately, which
488
   *              should only be attempted when it is known that the document
489
   *              has been fully rendered. Otherwise the internal map of ID
490
   *              attributes will be incomplete and scrolling will flounder.
491
   */
492
  private void scrollToParagraph( final int id, final boolean force ) {
493
    synchronized( mMutex ) {
494
      final var previewPane = getPreviewPane();
495
      final var scrollPane = previewPane.getScrollPane();
496
      final int approxId = getActiveEditorPane().approximateParagraphId( id );
497
498
      if( force ) {
499
        previewPane.scrollTo( approxId );
500
      }
501
      else {
502
        previewPane.tryScrollTo( approxId );
503
      }
504
505
      scrollPane.repaint();
506
    }
507
  }
508
509
  private void updateVariableNameInjector( final FileEditorTab tab ) {
510
    getVariableNameInjector().addListener( tab );
511
  }
512
513
  /**
514
   * Called whenever the preview pane becomes out of sync with the file editor
515
   * tab. This can be called when the text changes, the caret paragraph
516
   * changes, or the file tab changes.
517
   *
518
   * @param tab The file editor tab that has been changed in some fashion.
519
   */
520
  private void process( final FileEditorTab tab ) {
521
    if( tab != null ) {
522
      getPreviewPane().setPath( tab.getPath() );
523
524
      final Processor<String> processor = getProcessors().computeIfAbsent(
525
          tab, p -> createProcessors( tab )
526
      );
527
528
      try {
529
        processChain( processor, tab.getEditorText() );
530
      } catch( final Exception ex ) {
531
        error( ex );
532
      }
533
    }
534
  }
535
536
  /**
537
   * Executes the processing chain, operating on the given string.
538
   *
539
   * @param handler The first processor in the chain to call.
540
   * @param text    The initial value of the text to process.
541
   * @return The final value of the text that was processed by the chain.
542
   */
543
  private String processChain( Processor<String> handler, String text ) {
544
    while( handler != null && text != null ) {
545
      text = handler.process( text );
546
      handler = handler.next();
547
    }
548
549
    return text;
550
  }
551
552
  private void renderActiveTab() {
553
    process( getActiveFileEditorTab() );
554
  }
555
556
  /**
557
   * Called when a definition source is opened.
558
   *
559
   * @param path Path to the definition source that was opened.
560
   */
561
  private void openDefinitions( final Path path ) {
562
    try {
563
      final var ds = createDefinitionSource( path );
564
      setDefinitionSource( ds );
565
566
      final var prefs = getUserPreferences();
567
      prefs.definitionPathProperty().setValue( path.toFile() );
568
      prefs.save();
569
570
      final var tooltipPath = new Tooltip( path.toString() );
571
      tooltipPath.setShowDelay( Duration.millis( 200 ) );
572
573
      final var pane = getDefinitionPane();
574
      pane.update( ds );
575
      pane.addTreeChangeHandler( mTreeHandler );
576
      pane.addKeyEventHandler( mDefinitionKeyHandler );
577
      pane.filenameProperty().setValue( path.getFileName().toString() );
578
      pane.setTooltip( tooltipPath );
579
580
      interpolateResolvedMap();
581
    } catch( final Exception ex ) {
582
      error( ex );
583
    }
584
  }
585
586
  private void exportDefinitions( final Path path ) {
587
    try {
588
      final DefinitionPane pane = getDefinitionPane();
589
      final TreeItem<String> root = pane.getTreeView().getRoot();
590
      final TreeItem<String> problemChild = pane.isTreeWellFormed();
591
592
      if( problemChild == null ) {
593
        getDefinitionSource().getTreeAdapter().export( root, path );
594
        getNotifier().clear();
595
      }
596
      else {
597
        final String msg = get(
598
            "yaml.error.tree.form", problemChild.getValue() );
599
        error( msg );
600
      }
601
    } catch( final Exception ex ) {
602
      error( ex );
603
    }
604
  }
605
606
  private void interpolateResolvedMap() {
607
    final Map<String, String> treeMap = getDefinitionPane().toMap();
608
    final Map<String, String> map = new HashMap<>( treeMap );
609
    MapInterpolator.interpolate( map );
610
611
    getResolvedMap().clear();
612
    getResolvedMap().putAll( map );
613
  }
614
615
  private void initDefinitionPane() {
616
    openDefinitions( getDefinitionPath() );
617
  }
618
619
  /**
620
   * Called when an exception occurs that warrants the user's attention.
621
   *
622
   * @param ex The exception with a message that the user should know about.
623
   */
624
  private void error( final Exception ex ) {
625
    getNotifier().notify( ex );
626
  }
627
628
  private void error( final String msg ) {
629
    getNotifier().notify( msg );
630
  }
631
632
  //---- File actions -------------------------------------------------------
633
634
  /**
635
   * Called when an {@link Observable} instance has changed. This is called
636
   * by both the {@link Snitch} service and the notify service. The @link
637
   * Snitch} service can be called for different file types, including
638
   * {@link DefinitionSource} instances.
639
   *
640
   * @param observable The observed instance.
641
   * @param value      The noteworthy item.
642
   */
643
  @Override
644
  public void update( final Observable observable, final Object value ) {
645
    if( value != null ) {
646
      if( observable instanceof Snitch && value instanceof Path ) {
647
        updateSelectedTab();
648
      }
649
      else if( observable instanceof Notifier && value instanceof String ) {
650
        updateStatusBar( (String) value );
651
      }
652
    }
653
  }
654
655
  /**
656
   * Updates the status bar to show the given message.
657
   *
658
   * @param s The message to show in the status bar.
659
   */
660
  private void updateStatusBar( final String s ) {
661
    runLater(
662
        () -> {
663
          final int index = s.indexOf( '\n' );
664
          final String message = s.substring(
665
              0, index > 0 ? index : s.length() );
666
667
          getStatusBar().setText( message );
668
        }
669
    );
670
  }
671
672
  /**
673
   * Called when a file has been modified.
674
   */
675
  private void updateSelectedTab() {
676
    runLater(
677
        () -> {
678
          // Brute-force XSLT file reload by re-instantiating all processors.
679
          resetProcessors();
680
          renderActiveTab();
681
        }
682
    );
683
  }
684
685
  /**
686
   * After resetting the processors, they will refresh anew to be up-to-date
687
   * with the files (text and definition) currently loaded into the editor.
688
   */
689
  private void resetProcessors() {
690
    getProcessors().clear();
691
  }
692
693
  //---- File actions -------------------------------------------------------
694
695
  private void fileNew() {
696
    getFileEditorPane().newEditor();
697
  }
698
699
  private void fileOpen() {
700
    getFileEditorPane().openFileDialog();
701
  }
702
703
  private void fileClose() {
704
    getFileEditorPane().closeEditor( getActiveFileEditorTab(), true );
705
  }
706
707
  /**
708
   * TODO: Upon closing, first remove the tab change listeners. (There's no
709
   * need to re-render each tab when all are being closed.)
710
   */
711
  private void fileCloseAll() {
712
    getFileEditorPane().closeAllEditors();
713
  }
714
715
  private void fileSave() {
716
    getFileEditorPane().saveEditor( getActiveFileEditorTab() );
717
  }
718
719
  private void fileSaveAs() {
720
    final FileEditorTab editor = getActiveFileEditorTab();
721
    getFileEditorPane().saveEditorAs( editor );
722
    getProcessors().remove( editor );
723
724
    try {
725
      process( editor );
726
    } catch( final Exception ex ) {
727
      error( ex );
728
    }
729
  }
730
731
  private void fileSaveAll() {
732
    getFileEditorPane().saveAllEditors();
733
  }
734
735
  private void fileExit() {
736
    final Window window = getWindow();
737
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
738
  }
739
740
  //---- Edit actions -------------------------------------------------------
741
742
  /**
743
   * Transform the Markdown into HTML then copy that HTML into the copy
744
   * buffer.
745
   */
746
  private void copyHtml() {
747
    final var markdown = getActiveEditorPane().getText();
748
    final var processors = createProcessorFactory().createProcessors(
749
        getActiveFileEditorTab()
750
    );
751
752
    final var chain = processors.remove( HtmlPreviewProcessor.class );
753
754
    final String html = processChain( chain, markdown );
755
756
    final Clipboard clipboard = Clipboard.getSystemClipboard();
757
    final ClipboardContent content = new ClipboardContent();
758
    content.putString( html );
759
    clipboard.setContent( content );
760
  }
761
762
  /**
763
   * Used to find text in the active file editor window.
764
   */
765
  private void editFind() {
766
    final TextField input = getFindTextField();
767
    getStatusBar().setGraphic( input );
768
    input.requestFocus();
769
  }
770
771
  public void editFindNext() {
772
    getActiveFileEditorTab().searchNext( getFindTextField().getText() );
773
  }
774
775
  public void editPreferences() {
776
    getUserPreferences().show();
777
  }
778
779
  //---- Insert actions -----------------------------------------------------
780
781
  /**
782
   * Delegates to the active editor to handle wrapping the current text
783
   * selection with leading and trailing strings.
784
   *
785
   * @param leading  The string to put before the selection.
786
   * @param trailing The string to put after the selection.
787
   */
788
  private void insertMarkdown(
789
      final String leading, final String trailing ) {
790
    getActiveEditorPane().surroundSelection( leading, trailing );
791
  }
792
793
  private void insertMarkdown(
794
      final String leading, final String trailing, final String hint ) {
795
    getActiveEditorPane().surroundSelection( leading, trailing, hint );
796
  }
797
798
  //---- Help actions -------------------------------------------------------
799
800
  private void helpAbout() {
801
    final Alert alert = new Alert( AlertType.INFORMATION );
802
    alert.setTitle( get( "Dialog.about.title" ) );
803
    alert.setHeaderText( get( "Dialog.about.header" ) );
804
    alert.setContentText( get( "Dialog.about.content" ) );
805
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
806
    alert.initOwner( getWindow() );
807
808
    alert.showAndWait();
809
  }
810
811
  //---- Member creators ----------------------------------------------------
812
813
  private SpellChecker createSpellChecker() {
814
    try {
815
      final Collection<String> lexicon = readLexicon( "en.txt" );
816
      return SymSpellSpeller.forLexicon( lexicon );
817
    } catch( final Exception ex ) {
818
      error( ex );
819
      return new PermissiveSpeller();
820
    }
821
  }
822
823
  /**
824
   * Factory to create processors that are suited to different file types.
825
   *
826
   * @param tab The tab that is subjected to processing.
827
   * @return A processor suited to the file type specified by the tab's path.
828
   */
829
  private Processor<String> createProcessors( final FileEditorTab tab ) {
830
    return createProcessorFactory().createProcessors( tab );
831
  }
832
833
  private ProcessorFactory createProcessorFactory() {
834
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
835
  }
836
837
  private HTMLPreviewPane createHTMLPreviewPane() {
838
    return new HTMLPreviewPane();
839
  }
840
841
  private DefinitionSource createDefaultDefinitionSource() {
842
    return new YamlDefinitionSource( getDefinitionPath() );
843
  }
844
845
  private DefinitionSource createDefinitionSource( final Path path ) {
846
    try {
847
      return createDefinitionFactory().createDefinitionSource( path );
848
    } catch( final Exception ex ) {
849
      error( ex );
850
      return createDefaultDefinitionSource();
851
    }
852
  }
853
854
  private TextField createFindTextField() {
855
    return new TextField();
856
  }
857
858
  private DefinitionFactory createDefinitionFactory() {
859
    return new DefinitionFactory();
860
  }
861
862
  private StatusBar createStatusBar() {
863
    return new StatusBar();
864
  }
865
866
  private Scene createScene() {
867
    final SplitPane splitPane = new SplitPane(
868
        getDefinitionPane().getNode(),
869
        getFileEditorPane().getNode(),
870
        getPreviewPane().getNode() );
871
872
    splitPane.setDividerPositions(
873
        getFloat( K_PANE_SPLIT_DEFINITION, .22f ),
874
        getFloat( K_PANE_SPLIT_EDITOR, .60f ),
875
        getFloat( K_PANE_SPLIT_PREVIEW, .18f ) );
876
877
    getDefinitionPane().prefHeightProperty()
878
                       .bind( splitPane.heightProperty() );
879
880
    final BorderPane borderPane = new BorderPane();
881
    borderPane.setPrefSize( 1280, 800 );
882
    borderPane.setTop( createMenuBar() );
883
    borderPane.setBottom( getStatusBar() );
884
    borderPane.setCenter( splitPane );
885
886
    final VBox statusBar = new VBox();
887
    statusBar.setAlignment( Pos.BASELINE_CENTER );
888
    statusBar.getChildren().add( getLineNumberText() );
889
    getStatusBar().getRightItems().add( statusBar );
890
891
    // Force preview pane refresh on Windows.
892
    if( SystemUtils.IS_OS_WINDOWS ) {
893
      splitPane.getDividers().get( 1 ).positionProperty().addListener(
894
          ( l, oValue, nValue ) -> runLater(
895
              () -> getPreviewPane().getScrollPane().repaint()
896
          )
897
      );
898
    }
899
900
    return new Scene( borderPane );
901
  }
902
903
  private Text createLineNumberText() {
904
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
905
  }
906
907
  private Node createMenuBar() {
908
    final BooleanBinding activeFileEditorIsNull =
909
        getFileEditorPane().activeFileEditorProperty().isNull();
910
911
    // File actions
912
    final Action fileNewAction = new ActionBuilder()
913
        .setText( "Main.menu.file.new" )
914
        .setAccelerator( "Shortcut+N" )
915
        .setIcon( FILE_ALT )
916
        .setAction( e -> fileNew() )
917
        .build();
918
    final Action fileOpenAction = new ActionBuilder()
919
        .setText( "Main.menu.file.open" )
920
        .setAccelerator( "Shortcut+O" )
921
        .setIcon( FOLDER_OPEN_ALT )
922
        .setAction( e -> fileOpen() )
923
        .build();
924
    final Action fileCloseAction = new ActionBuilder()
925
        .setText( "Main.menu.file.close" )
926
        .setAccelerator( "Shortcut+W" )
927
        .setAction( e -> fileClose() )
928
        .setDisable( activeFileEditorIsNull )
929
        .build();
930
    final Action fileCloseAllAction = new ActionBuilder()
931
        .setText( "Main.menu.file.close_all" )
932
        .setAction( e -> fileCloseAll() )
933
        .setDisable( activeFileEditorIsNull )
934
        .build();
935
    final Action fileSaveAction = new ActionBuilder()
936
        .setText( "Main.menu.file.save" )
937
        .setAccelerator( "Shortcut+S" )
938
        .setIcon( FLOPPY_ALT )
939
        .setAction( e -> fileSave() )
940
        .setDisable( createActiveBooleanProperty(
941
            FileEditorTab::modifiedProperty ).not() )
942
        .build();
943
    final Action fileSaveAsAction = new ActionBuilder()
944
        .setText( "Main.menu.file.save_as" )
945
        .setAction( e -> fileSaveAs() )
946
        .setDisable( activeFileEditorIsNull )
947
        .build();
948
    final Action fileSaveAllAction = new ActionBuilder()
949
        .setText( "Main.menu.file.save_all" )
950
        .setAccelerator( "Shortcut+Shift+S" )
951
        .setAction( e -> fileSaveAll() )
952
        .setDisable( Bindings.not(
953
            getFileEditorPane().anyFileEditorModifiedProperty() ) )
954
        .build();
955
    final Action fileExitAction = new ActionBuilder()
956
        .setText( "Main.menu.file.exit" )
957
        .setAction( e -> fileExit() )
958
        .build();
959
960
    // Edit actions
961
    final Action editCopyHtmlAction = new ActionBuilder()
962
        .setText( Messages.get( "Main.menu.edit.copy.html" ) )
963
        .setIcon( HTML5 )
964
        .setAction( e -> copyHtml() )
965
        .setDisable( activeFileEditorIsNull )
966
        .build();
967
968
    final Action editUndoAction = new ActionBuilder()
969
        .setText( "Main.menu.edit.undo" )
970
        .setAccelerator( "Shortcut+Z" )
971
        .setIcon( UNDO )
972
        .setAction( e -> getActiveEditorPane().undo() )
973
        .setDisable( createActiveBooleanProperty(
974
            FileEditorTab::canUndoProperty ).not() )
975
        .build();
976
    final Action editRedoAction = new ActionBuilder()
977
        .setText( "Main.menu.edit.redo" )
978
        .setAccelerator( "Shortcut+Y" )
979
        .setIcon( REPEAT )
980
        .setAction( e -> getActiveEditorPane().redo() )
981
        .setDisable( createActiveBooleanProperty(
982
            FileEditorTab::canRedoProperty ).not() )
983
        .build();
984
985
    final Action editCutAction = new ActionBuilder()
986
        .setText( Messages.get( "Main.menu.edit.cut" ) )
987
        .setAccelerator( "Shortcut+X" )
988
        .setIcon( CUT )
989
        .setAction( e -> getActiveEditorPane().cut() )
990
        .setDisable( activeFileEditorIsNull )
991
        .build();
992
    final Action editCopyAction = new ActionBuilder()
993
        .setText( Messages.get( "Main.menu.edit.copy" ) )
994
        .setAccelerator( "Shortcut+C" )
995
        .setIcon( COPY )
996
        .setAction( e -> getActiveEditorPane().copy() )
997
        .setDisable( activeFileEditorIsNull )
998
        .build();
999
    final Action editPasteAction = new ActionBuilder()
1000
        .setText( Messages.get( "Main.menu.edit.paste" ) )
1001
        .setAccelerator( "Shortcut+V" )
1002
        .setIcon( PASTE )
1003
        .setAction( e -> getActiveEditorPane().paste() )
1004
        .setDisable( activeFileEditorIsNull )
1005
        .build();
1006
    final Action editSelectAllAction = new ActionBuilder()
1007
        .setText( Messages.get( "Main.menu.edit.selectAll" ) )
1008
        .setAccelerator( "Shortcut+A" )
1009
        .setAction( e -> getActiveEditorPane().selectAll() )
1010
        .setDisable( activeFileEditorIsNull )
1011
        .build();
1012
1013
    final Action editFindAction = new ActionBuilder()
1014
        .setText( "Main.menu.edit.find" )
1015
        .setAccelerator( "Ctrl+F" )
1016
        .setIcon( SEARCH )
1017
        .setAction( e -> editFind() )
1018
        .setDisable( activeFileEditorIsNull )
1019
        .build();
1020
    final Action editFindNextAction = new ActionBuilder()
1021
        .setText( "Main.menu.edit.find.next" )
1022
        .setAccelerator( "F3" )
1023
        .setIcon( null )
1024
        .setAction( e -> editFindNext() )
1025
        .setDisable( activeFileEditorIsNull )
1026
        .build();
1027
    final Action editPreferencesAction = new ActionBuilder()
1028
        .setText( "Main.menu.edit.preferences" )
1029
        .setAccelerator( "Ctrl+Alt+S" )
1030
        .setAction( e -> editPreferences() )
1031
        .build();
1032
1033
    // Insert actions
1034
    final Action insertBoldAction = new ActionBuilder()
1035
        .setText( "Main.menu.insert.bold" )
1036
        .setAccelerator( "Shortcut+B" )
1037
        .setIcon( BOLD )
1038
        .setAction( e -> insertMarkdown( "**", "**" ) )
1039
        .setDisable( activeFileEditorIsNull )
1040
        .build();
1041
    final Action insertItalicAction = new ActionBuilder()
1042
        .setText( "Main.menu.insert.italic" )
1043
        .setAccelerator( "Shortcut+I" )
1044
        .setIcon( ITALIC )
1045
        .setAction( e -> insertMarkdown( "*", "*" ) )
1046
        .setDisable( activeFileEditorIsNull )
1047
        .build();
1048
    final Action insertSuperscriptAction = new ActionBuilder()
1049
        .setText( "Main.menu.insert.superscript" )
1050
        .setAccelerator( "Shortcut+[" )
1051
        .setIcon( SUPERSCRIPT )
1052
        .setAction( e -> insertMarkdown( "^", "^" ) )
1053
        .setDisable( activeFileEditorIsNull )
1054
        .build();
1055
    final Action insertSubscriptAction = new ActionBuilder()
1056
        .setText( "Main.menu.insert.subscript" )
1057
        .setAccelerator( "Shortcut+]" )
1058
        .setIcon( SUBSCRIPT )
1059
        .setAction( e -> insertMarkdown( "~", "~" ) )
1060
        .setDisable( activeFileEditorIsNull )
1061
        .build();
1062
    final Action insertStrikethroughAction = new ActionBuilder()
1063
        .setText( "Main.menu.insert.strikethrough" )
1064
        .setAccelerator( "Shortcut+T" )
1065
        .setIcon( STRIKETHROUGH )
1066
        .setAction( e -> insertMarkdown( "~~", "~~" ) )
1067
        .setDisable( activeFileEditorIsNull )
1068
        .build();
1069
    final Action insertBlockquoteAction = new ActionBuilder()
1070
        .setText( "Main.menu.insert.blockquote" )
1071
        .setAccelerator( "Ctrl+Q" )
1072
        .setIcon( QUOTE_LEFT )
1073
        .setAction( e -> insertMarkdown( "\n\n> ", "" ) )
1074
        .setDisable( activeFileEditorIsNull )
1075
        .build();
1076
    final Action insertCodeAction = new ActionBuilder()
1077
        .setText( "Main.menu.insert.code" )
1078
        .setAccelerator( "Shortcut+K" )
1079
        .setIcon( CODE )
1080
        .setAction( e -> insertMarkdown( "`", "`" ) )
1081
        .setDisable( activeFileEditorIsNull )
1082
        .build();
1083
    final Action insertFencedCodeBlockAction = new ActionBuilder()
1084
        .setText( "Main.menu.insert.fenced_code_block" )
1085
        .setAccelerator( "Shortcut+Shift+K" )
1086
        .setIcon( FILE_CODE_ALT )
1087
        .setAction( e -> insertMarkdown(
1088
            "\n\n```\n",
1089
            "\n```\n\n",
1090
            get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
1091
        .setDisable( activeFileEditorIsNull )
1092
        .build();
1093
    final Action insertLinkAction = new ActionBuilder()
1094
        .setText( "Main.menu.insert.link" )
1095
        .setAccelerator( "Shortcut+L" )
1096
        .setIcon( LINK )
1097
        .setAction( e -> getActiveEditorPane().insertLink() )
1098
        .setDisable( activeFileEditorIsNull )
1099
        .build();
1100
    final Action insertImageAction = new ActionBuilder()
1101
        .setText( "Main.menu.insert.image" )
1102
        .setAccelerator( "Shortcut+G" )
1103
        .setIcon( PICTURE_ALT )
1104
        .setAction( e -> getActiveEditorPane().insertImage() )
1105
        .setDisable( activeFileEditorIsNull )
1106
        .build();
1107
1108
    // Number of heading actions (H1 ... H3)
1109
    final int HEADINGS = 3;
1110
    final Action[] headings = new Action[ HEADINGS ];
1111
1112
    for( int i = 1; i <= HEADINGS; i++ ) {
1113
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
1114
      final String markup = String.format( "%n%n%s ", hashes );
1115
      final String text = "Main.menu.insert.heading." + i;
1116
      final String accelerator = "Shortcut+" + i;
1117
      final String prompt = text + ".prompt";
1118
1119
      headings[ i - 1 ] = new ActionBuilder()
1120
          .setText( text )
1121
          .setAccelerator( accelerator )
1122
          .setIcon( HEADER )
1123
          .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
1124
          .setDisable( activeFileEditorIsNull )
1125
          .build();
1126
    }
1127
1128
    final Action insertUnorderedListAction = new ActionBuilder()
1129
        .setText( "Main.menu.insert.unordered_list" )
1130
        .setAccelerator( "Shortcut+U" )
1131
        .setIcon( LIST_UL )
1132
        .setAction( e -> insertMarkdown( "\n\n* ", "" ) )
1133
        .setDisable( activeFileEditorIsNull )
1134
        .build();
1135
    final Action insertOrderedListAction = new ActionBuilder()
1136
        .setText( "Main.menu.insert.ordered_list" )
1137
        .setAccelerator( "Shortcut+Shift+O" )
1138
        .setIcon( LIST_OL )
1139
        .setAction( e -> insertMarkdown(
1140
            "\n\n1. ", "" ) )
1141
        .setDisable( activeFileEditorIsNull )
1142
        .build();
1143
    final Action insertHorizontalRuleAction = new ActionBuilder()
1144
        .setText( "Main.menu.insert.horizontal_rule" )
1145
        .setAccelerator( "Shortcut+H" )
1146
        .setAction( e -> insertMarkdown(
1147
            "\n\n---\n\n", "" ) )
1148
        .setDisable( activeFileEditorIsNull )
1149
        .build();
1150
1151
    // Help actions
1152
    final Action helpAboutAction = new ActionBuilder()
1153
        .setText( "Main.menu.help.about" )
1154
        .setAction( e -> helpAbout() )
1155
        .build();
1156
1157
    //---- MenuBar ----
1158
    final Menu fileMenu = ActionUtils.createMenu(
1159
        get( "Main.menu.file" ),
1160
        fileNewAction,
1161
        fileOpenAction,
1162
        null,
1163
        fileCloseAction,
1164
        fileCloseAllAction,
1165
        null,
1166
        fileSaveAction,
1167
        fileSaveAsAction,
1168
        fileSaveAllAction,
1169
        null,
1170
        fileExitAction );
1171
1172
    final Menu editMenu = ActionUtils.createMenu(
1173
        get( "Main.menu.edit" ),
1174
        editCopyHtmlAction,
1175
        null,
1176
        editUndoAction,
1177
        editRedoAction,
1178
        null,
1179
        editCutAction,
1180
        editCopyAction,
1181
        editPasteAction,
1182
        editSelectAllAction,
1183
        null,
1184
        editFindAction,
1185
        editFindNextAction,
1186
        null,
1187
        editPreferencesAction );
1188
1189
    final Menu insertMenu = ActionUtils.createMenu(
1190
        get( "Main.menu.insert" ),
1191
        insertBoldAction,
1192
        insertItalicAction,
1193
        insertSuperscriptAction,
1194
        insertSubscriptAction,
1195
        insertStrikethroughAction,
1196
        insertBlockquoteAction,
1197
        insertCodeAction,
1198
        insertFencedCodeBlockAction,
1199
        null,
1200
        insertLinkAction,
1201
        insertImageAction,
1202
        null,
1203
        headings[ 0 ],
1204
        headings[ 1 ],
1205
        headings[ 2 ],
1206
        null,
1207
        insertUnorderedListAction,
1208
        insertOrderedListAction,
1209
        insertHorizontalRuleAction
1210
    );
1211
1212
    final Menu helpMenu = ActionUtils.createMenu(
1213
        get( "Main.menu.help" ),
1214
        helpAboutAction );
1215
1216
    final MenuBar menuBar = new MenuBar(
1217
        fileMenu,
1218
        editMenu,
1219
        insertMenu,
1220
        helpMenu );
1221
1222
    //---- ToolBar ----
1223
    final ToolBar toolBar = ActionUtils.createToolBar(
1224
        fileNewAction,
1225
        fileOpenAction,
1226
        fileSaveAction,
1227
        null,
1228
        editUndoAction,
1229
        editRedoAction,
1230
        editCutAction,
1231
        editCopyAction,
1232
        editPasteAction,
1233
        null,
1234
        insertBoldAction,
1235
        insertItalicAction,
1236
        insertSuperscriptAction,
1237
        insertSubscriptAction,
1238
        insertBlockquoteAction,
1239
        insertCodeAction,
1240
        insertFencedCodeBlockAction,
1241
        null,
1242
        insertLinkAction,
1243
        insertImageAction,
1244
        null,
1245
        headings[ 0 ],
1246
        null,
1247
        insertUnorderedListAction,
1248
        insertOrderedListAction );
1249
1250
    return new VBox( menuBar, toolBar );
1251
  }
1252
1253
  /**
1254
   * Creates a boolean property that is bound to another boolean value of the
1255
   * active editor.
1256
   */
1257
  private BooleanProperty createActiveBooleanProperty(
1258
      final Function<FileEditorTab, ObservableBooleanValue> func ) {
1259
1260
    final BooleanProperty b = new SimpleBooleanProperty();
1261
    final FileEditorTab tab = getActiveFileEditorTab();
1262
1263
    if( tab != null ) {
1264
      b.bind( func.apply( tab ) );
1265
    }
1266
1267
    getFileEditorPane().activeFileEditorProperty().addListener(
1268
        ( observable, oldFileEditor, newFileEditor ) -> {
1269
          b.unbind();
1270
1271
          if( newFileEditor == null ) {
1272
            b.set( false );
1273
          }
1274
          else {
1275
            b.bind( func.apply( newFileEditor ) );
1276
          }
1277
        }
1278
    );
1279
1280
    return b;
1281
  }
1282
1283
  //---- Convenience accessors ----------------------------------------------
1284
1285
  private Preferences getPreferences() {
1286
    return sOptions.getState();
1287
  }
1288
1289
  private int getCurrentParagraphIndex() {
1290
    return getActiveEditorPane().getCurrentParagraphIndex();
1291
  }
1292
1293
  private float getFloat( final String key, final float defaultValue ) {
1294
    return getPreferences().getFloat( key, defaultValue );
1295
  }
1296
1297
  public Window getWindow() {
1298
    return getScene().getWindow();
1299
  }
1300
1301
  private MarkdownEditorPane getActiveEditorPane() {
1302
    return getActiveFileEditorTab().getEditorPane();
1303
  }
1304
1305
  private FileEditorTab getActiveFileEditorTab() {
1306
    return getFileEditorPane().getActiveFileEditor();
1307
  }
1308
1309
  //---- Member accessors ---------------------------------------------------
1310
1311
  protected Scene getScene() {
1312
    return mScene;
1313
  }
1314
1315
  private SpellChecker getSpellChecker() {
1316
    return mSpellChecker;
1317
  }
1318
1319
  private Map<FileEditorTab, Processor<String>> getProcessors() {
1320
    return mProcessors;
1321
  }
1322
1323
  private FileEditorTabPane getFileEditorPane() {
1324
    return mFileEditorPane;
1325
  }
1326
1327
  private HTMLPreviewPane getPreviewPane() {
1328
    return mPreviewPane;
1329
  }
1330
1331
  private void setDefinitionSource(
1332
      final DefinitionSource definitionSource ) {
1333
    assert definitionSource != null;
1334
    mDefinitionSource = definitionSource;
1335
  }
1336
1337
  private DefinitionSource getDefinitionSource() {
1338
    return mDefinitionSource;
1339
  }
1340
1341
  private DefinitionPane getDefinitionPane() {
1342
    return mDefinitionPane;
1343
  }
1344
1345
  private Text getLineNumberText() {
1346
    return mLineNumberText;
1347
  }
1348
1349
  private StatusBar getStatusBar() {
1350
    return mStatusBar;
1351
  }
1352
1353
  private TextField getFindTextField() {
1354
    return mFindTextField;
1355
  }
1356
1357
  private VariableNameInjector getVariableNameInjector() {
1358
    return mVariableNameInjector;
1359
  }
1360
1361
  /**
1362
   * Returns the variable map of interpolated definitions.
1363
   *
1364
   * @return A map to help dereference variables.
1365
   */
1366
  private Map<String, String> getResolvedMap() {
1367
    return mResolvedMap;
1368
  }
1369
1370
  private Notifier getNotifier() {
1371
    return sNotifier;
1372
  }
1373
1374
  //---- Persistence accessors ----------------------------------------------
1375
1376
  private UserPreferences getUserPreferences() {
1377
    return sOptions.getUserPreferences();
1378
  }
1379
1380
  private Path getDefinitionPath() {
1381
    return getUserPreferences().getDefinitionPath();
1382
  }
1383
1384
  //---- Spelling -----------------------------------------------------------
1385
1386
  /**
1387
   * Delegates to {@link #spellcheck(StyleClassedTextArea, String, int)}.
1388
   * This is called to spell check the document, rather than a single paragraph.
1389
   *
1390
   * @param text The full document text.
1391
   */
1392
  private void spellcheck(
1393
      final StyleClassedTextArea editor, final String text ) {
1394
    spellcheck( editor, text, -1 );
1395
  }
1396
1397
  /**
1398
   * Spellchecks a subset of the entire document.
1399
   *
1400
   * @param text   Look up words for this text in the lexicon.
1401
   * @param paraId Set to -1 to apply resulting style spans to the entire
1402
   *               text.
1403
   */
1404
  private void spellcheck(
1405
      final StyleClassedTextArea editor, final String text, final int paraId ) {
1406
    final var builder = new StyleSpansBuilder<Collection<String>>();
1407
    final var runningIndex = new AtomicInteger( 0 );
1408
    final var checker = getSpellChecker();
1409
1410
    // The text nodes must be relayed through a contextual "visitor" that
1411
    // can return text in chunks with correlative offsets into the string.
1412
    // This allows Markdown, R Markdown, XML, and R XML documents to return
1413
    // sets of words to check.
1414
1415
    final var node = mParser.parse( text );
1416
    final var visitor = new TextVisitor( ( visited, bIndex, eIndex ) -> {
1417
      // Treat hyphenated compound words as individual words.
1418
      final var check = visited.replace( '-', ' ' );
1419
1420
      checker.proofread( check, ( misspelled, prevIndex, currIndex ) -> {
1421
        prevIndex += bIndex;
1422
        currIndex += bIndex;
1423
1424
        // Clear styling between lexiconically absent words.
1425
        builder.add( emptyList(), prevIndex - runningIndex.get() );
1426
        builder.add( singleton( "spelling" ), currIndex - prevIndex );
1427
        runningIndex.set( currIndex );
1428
      } );
1429
    } );
1430
1431
    visitor.visit( node );
1432
1433
    // If the running index was set, at least one word triggered the listener.
1434
    if( runningIndex.get() > 0 ) {
1435
      // Clear styling after the last lexiconically absent word.
1436
      builder.add( emptyList(), text.length() - runningIndex.get() );
1437
1438
      final var spans = builder.create();
1439
1440
      if( paraId >= 0 ) {
1441
        editor.setStyleSpans( paraId, 0, spans );
1442
      }
1443
      else {
1444
        editor.setStyleSpans( 0, spans );
1445
      }
1446
    }
1447
  }
1448
1449
  @SuppressWarnings("SameParameterValue")
1450
  private Collection<String> readLexicon( final String filename )
1451
      throws Exception {
1452
    final var path = Paths.get( LEXICONS_DIRECTORY, filename ).toString();
1453
    final var classLoader = MainWindow.class.getClassLoader();
1454
1455
    try( final var resource = classLoader.getResourceAsStream( path ) ) {
1456
      assert resource != null;
1457
1458
      return new BufferedReader( new InputStreamReader( resource, UTF_8 ) )
1459
          .lines()
1460
          .collect( Collectors.toList() );
1461
    }
1462
  }
1463
1464
  // TODO: Replace using Markdown processor instantiated for Markdown files.
1465
  // FIXME: https://github.com/DaveJarvis/scrivenvar/issues/59
1466
  private final Parser mParser = Parser.builder().build();
1467
1468
  // TODO: Replace with generic interface; provide Markdown/XML implementations.
1469
  // FIXME: https://github.com/DaveJarvis/scrivenvar/issues/59
1470
  private final static class TextVisitor {
1471
    private final NodeVisitor mVisitor = new NodeVisitor( new VisitHandler<>(
1472
        com.vladsch.flexmark.ast.Text.class, this::visit )
1473
    );
1474
1475
    private final SpellCheckListener mConsumer;
1476
1477
    public TextVisitor( final SpellCheckListener consumer ) {
1478
      mConsumer = consumer;
1479
    }
1480
1481
    private void visit( final com.vladsch.flexmark.util.ast.Node node ) {
1482
      if( node instanceof com.vladsch.flexmark.ast.Text ) {
1483
        mConsumer.accept( node.getChars().toString(),
1484
                          node.getStartOffset(),
1485
                          node.getEndOffset() );
1486
      }
1487
1488
      mVisitor.visitChildren( node );
1489
    }
1490
  }
1491
}
11492
A src/main/java/com/scrivenvar/Messages.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
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 java.text.MessageFormat;
30
import java.util.ResourceBundle;
31
import java.util.Stack;
32
33
import static com.scrivenvar.Constants.APP_BUNDLE_NAME;
34
import static java.util.ResourceBundle.getBundle;
35
36
/**
37
 * Recursively resolves message properties. Property values can refer to other
38
 * properties using a <code>${var}</code> syntax.
39
 */
40
public class Messages {
41
42
  private static final ResourceBundle RESOURCE_BUNDLE =
43
      getBundle( APP_BUNDLE_NAME );
44
45
  private Messages() {
46
  }
47
48
  /**
49
   * Return the value of a resource bundle value after having resolved any
50
   * references to other bundle variables.
51
   *
52
   * @param props The bundle containing resolvable properties.
53
   * @param s     The value for a key to resolve.
54
   * @return The value of the key with all references recursively dereferenced.
55
   */
56
  @SuppressWarnings("SameParameterValue")
57
  private static String resolve( final ResourceBundle props, final String s ) {
58
    final int len = s.length();
59
    final Stack<StringBuilder> stack = new Stack<>();
60
61
    StringBuilder sb = new StringBuilder( 256 );
62
    boolean open = false;
63
64
    for( int i = 0; i < len; i++ ) {
65
      final char c = s.charAt( i );
66
67
      switch( c ) {
68
        case '$': {
69
          if( i + 1 < len && s.charAt( i + 1 ) == '{' ) {
70
            stack.push( sb );
71
            sb = new StringBuilder( 256 );
72
            i++;
73
            open = true;
74
          }
75
76
          break;
77
        }
78
79
        case '}': {
80
          if( open ) {
81
            open = false;
82
            final String name = sb.toString();
83
84
            sb = stack.pop();
85
            sb.append( props.getString( name ) );
86
            break;
87
          }
88
        }
89
90
        default: {
91
          sb.append( c );
92
          break;
93
        }
94
      }
95
    }
96
97
    if( open ) {
98
      throw new IllegalArgumentException( "missing '}'" );
99
    }
100
101
    return sb.toString();
102
  }
103
104
  /**
105
   * Returns the value for a key from the message bundle.
106
   *
107
   * @param key Retrieve the value for this key.
108
   * @return The value for the key.
109
   */
110
  public static String get( final String key ) {
111
    try {
112
      return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
113
    } catch( final Exception ex ) {
114
      return key;
115
    }
116
  }
117
118
  public static String getLiteral( final String key ) {
119
    return RESOURCE_BUNDLE.getString( key );
120
  }
121
122
  public static String get( final String key, final boolean interpolate ) {
123
    return interpolate ? get( key ) : getLiteral( key );
124
  }
125
126
  /**
127
   * Returns the value for a key from the message bundle with the arguments
128
   * replacing <code>{#}</code> place holders.
129
   *
130
   * @param key  Retrieve the value for this key.
131
   * @param args The values to substitute for place holders.
132
   * @return The value for the key.
133
   */
134
  public static String get( final String key, final Object... args ) {
135
    return MessageFormat.format( get( key ), args );
136
  }
137
}
1138
A src/main/java/com/scrivenvar/ScrollEventHandler.java
1
/*
2
 * Copyright 2020 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 javafx.beans.property.BooleanProperty;
31
import javafx.beans.property.SimpleBooleanProperty;
32
import javafx.event.Event;
33
import javafx.event.EventHandler;
34
import javafx.scene.Node;
35
import javafx.scene.control.ScrollBar;
36
import javafx.scene.control.skin.ScrollBarSkin;
37
import javafx.scene.input.MouseEvent;
38
import javafx.scene.input.ScrollEvent;
39
import javafx.scene.layout.StackPane;
40
import org.fxmisc.flowless.VirtualizedScrollPane;
41
import org.fxmisc.richtext.StyleClassedTextArea;
42
43
import javax.swing.*;
44
45
import static javafx.geometry.Orientation.VERTICAL;
46
47
/**
48
 * Converts scroll events from {@link VirtualizedScrollPane} scroll bars to
49
 * an instance of {@link JScrollBar}.
50
 * <p>
51
 * Called to synchronize the scrolling areas for either scrolling with the
52
 * mouse or scrolling using the scrollbar's thumb. Both are required to avoid
53
 * scrolling on the estimatedScrollYProperty that occurs when text events
54
 * fire. Scrolling performed for text events are handled separately to ensure
55
 * the preview panel scrolls to the same position in the Markdown editor,
56
 * taking into account things like images, tables, and other potentially
57
 * long vertical presentation items.
58
 * </p>
59
 */
60
public final class ScrollEventHandler implements EventHandler<Event> {
61
62
  private final class MouseHandler implements EventHandler<MouseEvent> {
63
    private final EventHandler<? super MouseEvent> mOldHandler;
64
65
    /**
66
     * Constructs a new handler for mouse scrolling events.
67
     *
68
     * @param oldHandler Receives the event after scrolling takes place.
69
     */
70
    private MouseHandler( final EventHandler<? super MouseEvent> oldHandler ) {
71
      mOldHandler = oldHandler;
72
    }
73
74
    @Override
75
    public void handle( final MouseEvent event ) {
76
      ScrollEventHandler.this.handle( event );
77
      mOldHandler.handle( event );
78
    }
79
  }
80
81
  private final class ScrollHandler implements EventHandler<ScrollEvent> {
82
    @Override
83
    public void handle( final ScrollEvent event ) {
84
      ScrollEventHandler.this.handle( event );
85
    }
86
  }
87
88
  private final VirtualizedScrollPane<StyleClassedTextArea> mEditorScrollPane;
89
  private final JScrollBar mPreviewScrollBar;
90
  private final BooleanProperty mEnabled = new SimpleBooleanProperty();
91
92
  /**
93
   * @param editorScrollPane Scroll event source (human movement).
94
   * @param previewScrollBar Scroll event destination (corresponding movement).
95
   */
96
  public ScrollEventHandler(
97
      final VirtualizedScrollPane<StyleClassedTextArea> editorScrollPane,
98
      final JScrollBar previewScrollBar ) {
99
    mEditorScrollPane = editorScrollPane;
100
    mPreviewScrollBar = previewScrollBar;
101
102
    mEditorScrollPane.addEventFilter( ScrollEvent.ANY, new ScrollHandler() );
103
104
    final var thumb = getVerticalScrollBarThumb( mEditorScrollPane );
105
    thumb.setOnMouseDragged( new MouseHandler( thumb.getOnMouseDragged() ) );
106
  }
107
108
  /**
109
   * Gets a property intended to be bound to selected property of the tab being
110
   * scrolled. This is required because there's only one preview pane but
111
   * multiple editor panes. Each editor pane maintains its own scroll position.
112
   *
113
   * @return A {@link BooleanProperty} representing whether the scroll
114
   * events for this tab are to be executed.
115
   */
116
  public BooleanProperty enabledProperty() {
117
    return mEnabled;
118
  }
119
120
  /**
121
   * Scrolls the preview scrollbar relative to the edit scrollbar. Algorithm
122
   * is based on Karl Tauber's ratio calculation.
123
   *
124
   * @param event Unused; either {@link MouseEvent} or {@link ScrollEvent}
125
   */
126
  @Override
127
  public void handle( final Event event ) {
128
    if( isEnabled() ) {
129
      final var eScrollPane = getEditorScrollPane();
130
      final int eScrollY =
131
          eScrollPane.estimatedScrollYProperty().getValue().intValue();
132
      final int eHeight = (int)
133
          (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
134
              - eScrollPane.getHeight());
135
      final double eRatio = eHeight > 0
136
          ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
137
138
      final var pScrollBar = getPreviewScrollBar();
139
      final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight();
140
      final var pScrollY = (int) (pHeight * eRatio);
141
142
      pScrollBar.setValue( pScrollY );
143
      pScrollBar.getParent().repaint();
144
    }
145
  }
146
147
  private StackPane getVerticalScrollBarThumb(
148
      final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
149
    final ScrollBar scrollBar = getVerticalScrollBar( pane );
150
    final ScrollBarSkin skin = (ScrollBarSkin) (scrollBar.skinProperty().get());
151
152
    for( final Node node : skin.getChildren() ) {
153
      // Brittle, but what can you do?
154
      if( node.getStyleClass().contains( "thumb" ) ) {
155
        return (StackPane) node;
156
      }
157
    }
158
159
    throw new IllegalArgumentException( "No scroll bar skin found." );
160
  }
161
162
  private ScrollBar getVerticalScrollBar(
163
      final VirtualizedScrollPane<StyleClassedTextArea> pane ) {
164
165
    for( final Node node : pane.getChildrenUnmodifiable() ) {
166
      if( node instanceof ScrollBar ) {
167
        final ScrollBar scrollBar = (ScrollBar) node;
168
169
        if( scrollBar.getOrientation() == VERTICAL ) {
170
          return scrollBar;
171
        }
172
      }
173
    }
174
175
    throw new IllegalArgumentException( "No vertical scroll pane found." );
176
  }
177
178
  private boolean isEnabled() {
179
    // TODO: As a minor optimization, when this is set to false, it could remove
180
    // the MouseHandler and ScrollHandler so that events only dispatch to one
181
    // object (instead of one per editor tab).
182
    return mEnabled.get();
183
  }
184
185
  private VirtualizedScrollPane<StyleClassedTextArea> getEditorScrollPane() {
186
    return mEditorScrollPane;
187
  }
188
189
  private JScrollBar getPreviewScrollBar() {
190
    return mPreviewScrollBar;
191
  }
192
}
1193
A src/main/java/com/scrivenvar/Services.java
1
/*
2
 * Copyright 2020 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
public class Services {
39
40
  @SuppressWarnings("rawtypes")
41
  private static final Map<Class, Object> SINGLETONS = new HashMap<>();
42
43
  /**
44
   * Loads a service based on its interface definition. This will return an
45
   * existing instance if the class has already been instantiated.
46
   *
47
   * @param <T> The service to load.
48
   * @param api The interface definition for the service.
49
   * @return A class that implements the interface.
50
   */
51
  @SuppressWarnings("unchecked")
52
  public static <T> T load( final Class<T> api ) {
53
    final T o = (T) get( api );
54
55
    return o == null ? newInstance( api ) : o;
56
  }
57
58
  private static <T> T newInstance( final Class<T> api ) {
59
    final ServiceLoader<T> services = ServiceLoader.load( api );
60
61
    for( final T service : services ) {
62
      if( service != null ) {
63
        // Re-use the same instance the next time the class is loaded.
64
        put( api, service );
65
        return service;
66
      }
67
    }
68
69
    throw new RuntimeException( "No implementation for: " + api );
70
  }
71
72
  @SuppressWarnings("rawtypes")
73
  private static void put( final Class key, Object value ) {
74
    SINGLETONS.put( key, value );
75
  }
76
77
  @SuppressWarnings("rawtypes")
78
  private static Object get( final Class api ) {
79
    return SINGLETONS.get( api );
80
  }
81
}
182
A src/main/java/com/scrivenvar/controls/BrowseFileButton.java
1
/*
2
 * Copyright 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 com.scrivenvar.Messages;
31
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
32
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
33
import javafx.beans.property.ObjectProperty;
34
import javafx.beans.property.SimpleObjectProperty;
35
import javafx.event.ActionEvent;
36
import javafx.scene.control.Button;
37
import javafx.scene.control.Tooltip;
38
import javafx.scene.input.KeyCode;
39
import javafx.scene.input.KeyEvent;
40
import javafx.stage.FileChooser;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
43
import java.io.File;
44
import java.nio.file.Path;
45
import java.util.ArrayList;
46
import java.util.List;
47
48
/**
49
 * Button that opens a file chooser to select a local file for a URL.
50
 */
51
public class BrowseFileButton extends Button {
52
  private final List<ExtensionFilter> extensionFilters = new ArrayList<>();
53
54
  public BrowseFileButton() {
55
    setGraphic(
56
        FontAwesomeIconFactory.get().createIcon( FontAwesomeIcon.FILE_ALT )
57
    );
58
    setTooltip( new Tooltip( Messages.get( "BrowseFileButton.tooltip" ) ) );
59
    setOnAction( this::browse );
60
61
    disableProperty().bind( basePath.isNull() );
62
63
    // workaround for a JavaFX bug:
64
    //   avoid closing the dialog that contains this control when the user
65
    //   closes the FileChooser or DirectoryChooser using the ESC key
66
    addEventHandler( KeyEvent.KEY_RELEASED, e -> {
67
      if( e.getCode() == KeyCode.ESCAPE ) {
68
        e.consume();
69
      }
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
80
  public Path getBasePath() {
81
    return basePath.get();
82
  }
83
84
  public void setBasePath( Path basePath ) {
85
    this.basePath.set( basePath );
86
  }
87
88
  // 'url' property
89
  private final ObjectProperty<String> url = new SimpleObjectProperty<>();
90
91
  public ObjectProperty<String> urlProperty() {
92
    return url;
93
  }
94
95
  protected void browse( ActionEvent e ) {
96
    FileChooser fileChooser = new FileChooser();
97
    fileChooser.setTitle( Messages.get( "BrowseFileButton.chooser.title" ) );
98
    fileChooser.getExtensionFilters().addAll( extensionFilters );
99
    fileChooser.getExtensionFilters()
100
               .add( new ExtensionFilter( Messages.get(
101
                   "BrowseFileButton.chooser.allFilesFilter" ), "*.*" ) );
102
    fileChooser.setInitialDirectory( getInitialDirectory() );
103
    File result = fileChooser.showOpenDialog( getScene().getWindow() );
104
    if( result != null ) {
105
      updateUrl( result );
106
    }
107
  }
108
109
  protected File getInitialDirectory() {
110
    //TODO build initial directory based on current value of 'url' property
111
    return getBasePath().toFile();
112
  }
113
114
  protected void updateUrl( File file ) {
115
    String newUrl;
116
    try {
117
      newUrl = getBasePath().relativize( file.toPath() ).toString();
118
    } catch( IllegalArgumentException ex ) {
119
      newUrl = file.toString();
120
    }
121
    url.set( newUrl.replace( '\\', '/' ) );
122
  }
123
}
1124
A src/main/java/com/scrivenvar/controls/EscapeTextField.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
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
35
/**
36
 * Responsible for escaping/unescaping characters for markdown.
37
 */
38
public class EscapeTextField extends TextField {
39
40
  public EscapeTextField() {
41
    escapedText.bindBidirectional(
42
        textProperty(),
43
        new StringConverter<>() {
44
          @Override
45
          public String toString( String object ) {
46
            return escape( object );
47
          }
48
49
          @Override
50
          public String fromString( String string ) {
51
            return unescape( string );
52
          }
53
        }
54
    );
55
    escapeCharacters.addListener(
56
        e -> escapedText.set( escape( textProperty().get() ) )
57
    );
58
  }
59
60
  // 'escapedText' property
61
  private final StringProperty escapedText = new SimpleStringProperty();
62
63
  public StringProperty escapedTextProperty() {
64
    return escapedText;
65
  }
66
67
  // 'escapeCharacters' property
68
  private final StringProperty escapeCharacters = new SimpleStringProperty();
69
70
  public String getEscapeCharacters() {
71
    return escapeCharacters.get();
72
  }
73
74
  public void setEscapeCharacters( String escapeCharacters ) {
75
    this.escapeCharacters.set( escapeCharacters );
76
  }
77
78
  private String escape( final String s ) {
79
    final String escapeChars = getEscapeCharacters();
80
81
    return isEmpty( escapeChars ) ? s :
82
        s.replaceAll( "([" + escapeChars.replaceAll(
83
            "(.)",
84
            "\\\\$1" ) + "])", "\\\\$1" );
85
  }
86
87
  private String unescape( final String s ) {
88
    final String escapeChars = getEscapeCharacters();
89
90
    return isEmpty( escapeChars ) ? s :
91
        s.replaceAll( "\\\\([" + escapeChars
92
            .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" );
93
  }
94
95
  private static boolean isEmpty( final String s ) {
96
    return s == null || s.isEmpty();
97
  }
98
}
199
A src/main/java/com/scrivenvar/decorators/RVariableDecorator.java
1
/*
2
 * Copyright 2020 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
import com.scrivenvar.Services;
31
import com.scrivenvar.preferences.UserPreferences;
32
import com.scrivenvar.service.Options;
33
34
/**
35
 * Brackets variable names with {@code `r#} and {@code `}.
36
 */
37
public class RVariableDecorator implements VariableDecorator {
38
  private static final Options sOptions = Services.load( Options.class );
39
40
  public static final String PREFIX = "`r#";
41
  public static final char SUFFIX = '`';
42
43
  private final String mDelimiterBegan =
44
      getUserPreferences().getRDelimiterBegan();
45
  private final String mDelimiterEnded =
46
      getUserPreferences().getRDelimiterEnded();
47
48
  /**
49
   * Returns the given string R-escaping backticks prepended and appended. This
50
   * is not null safe. Do not pass null into this method.
51
   *
52
   * @param variableName The string to decorate.
53
   * @return "`r#" + delimiterBegan + variableName+ delimiterEnded + "`".
54
   */
55
  @Override
56
  public String decorate( String variableName ) {
57
    assert variableName != null;
58
59
    // Delete the $ $ sigils from Markdown variables.
60
    if( variableName.length() > 1 ) {
61
      variableName = variableName.substring( 1, variableName.length() - 1 );
62
    }
63
64
    return PREFIX
65
        + mDelimiterBegan
66
        + "v$"
67
        + variableName.replace( '.', '$' )
68
        + mDelimiterEnded
69
        + SUFFIX;
70
  }
71
72
  private UserPreferences getUserPreferences() {
73
    return sOptions.getUserPreferences();
74
  }
75
}
176
A src/main/java/com/scrivenvar/decorators/VariableDecorator.java
1
/*
2
 * Copyright 2020 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 "
42
   *                     .Rmd" goes to `r#VAR`).
43
   * @return The given variable name modified with its requisite delimiters.
44
   */
45
  String decorate( String variableName );
46
}
147
A src/main/java/com/scrivenvar/decorators/YamlVariableDecorator.java
1
/*
2
 * Copyright 2020 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
public class YamlVariableDecorator implements VariableDecorator {
34
35
  /**
36
   * Returns the given {@link String} verbatim because variables in YAML
37
   * documents and plain Markdown documents already have the appropriate
38
   * tokenizable syntax wrapped around the text.
39
   *
40
   * @param variableName Returned verbatim.
41
   */
42
  @Override
43
  public String decorate( final String variableName ) {
44
    assert variableName != null;
45
    return variableName;
46
  }
47
48
  /**
49
   * Sigilifies the given key.
50
   *
51
   * @param key The key to adorn with YAML variable sigil characters.
52
   * @return The given key bracketed by dollar symbols.
53
   */
54
  public static String entoken( final String key ) {
55
    assert key != null;
56
    return '$' + key + '$';
57
  }
58
}
159
A src/main/java/com/scrivenvar/definition/DefinitionFactory.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.FileType;
32
import com.scrivenvar.definition.yaml.YamlDefinitionSource;
33
import com.scrivenvar.util.ProtocolResolver;
34
35
import java.nio.file.Path;
36
37
import static com.scrivenvar.Constants.DEFINITION_PROTOCOL_FILE;
38
import static com.scrivenvar.Constants.GLOB_PREFIX_DEFINITION;
39
import static com.scrivenvar.FileType.YAML;
40
41
/**
42
 * Responsible for creating objects that can read and write definition data
43
 * sources. The data source could be YAML, TOML, JSON, flat files, or from a
44
 * database.
45
 */
46
public class DefinitionFactory extends AbstractFileFactory {
47
48
  /**
49
   * Default (empty) constructor.
50
   */
51
  public DefinitionFactory() {
52
  }
53
54
  /**
55
   * Creates a definition source capable of reading definitions from the given
56
   * path.
57
   *
58
   * @param path Path to a resource containing definitions.
59
   * @return The definition source appropriate for the given path.
60
   */
61
  public DefinitionSource createDefinitionSource( final Path path ) {
62
    assert path != null;
63
64
    final String protocol = ProtocolResolver.getProtocol( path.toString() );
65
    DefinitionSource result = null;
66
67
    if( DEFINITION_PROTOCOL_FILE.equals( protocol ) ) {
68
      final FileType filetype = lookup( path, GLOB_PREFIX_DEFINITION );
69
      result = createFileDefinitionSource( filetype, path );
70
    }
71
    else {
72
      unknownFileType( protocol, path.toString() );
73
    }
74
75
    return result;
76
  }
77
78
  /**
79
   * Creates a definition source based on the file type.
80
   *
81
   * @param filetype Property key name suffix from settings.properties file.
82
   * @param path     Path to the file that corresponds to the extension.
83
   * @return A DefinitionSource capable of parsing the data stored at the path.
84
   */
85
  private DefinitionSource createFileDefinitionSource(
86
      final FileType filetype, final Path path ) {
87
    assert filetype != null;
88
    assert path != null;
89
90
    if( filetype == YAML ) {
91
      return new YamlDefinitionSource( path );
92
    }
93
94
    throw new IllegalArgumentException( filetype.toString() );
95
  }
96
}
197
A src/main/java/com/scrivenvar/definition/DefinitionPane.java
1
/*
2
 * Copyright 2020 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 de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
31
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
32
import javafx.beans.property.SimpleStringProperty;
33
import javafx.beans.property.StringProperty;
34
import javafx.collections.ObservableList;
35
import javafx.event.ActionEvent;
36
import javafx.event.Event;
37
import javafx.event.EventHandler;
38
import javafx.geometry.Insets;
39
import javafx.geometry.Pos;
40
import javafx.scene.Node;
41
import javafx.scene.control.*;
42
import javafx.scene.control.cell.TextFieldTreeCell;
43
import javafx.scene.input.KeyEvent;
44
import javafx.scene.layout.BorderPane;
45
import javafx.scene.layout.HBox;
46
import javafx.util.StringConverter;
47
48
import java.util.*;
49
50
import static com.scrivenvar.Messages.get;
51
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
52
import static javafx.geometry.Pos.CENTER;
53
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
54
55
/**
56
 * Provides the user interface that holds a {@link TreeView}, which
57
 * allows users to interact with key/value pairs loaded from the
58
 * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
59
 */
60
public final class DefinitionPane extends BorderPane {
61
62
  /**
63
   * Contains a view of the definitions.
64
   */
65
  private final TreeView<String> mTreeView = new TreeView<>();
66
67
  /**
68
   * Handlers for key press events.
69
   */
70
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
71
      = new HashSet<>();
72
73
  /**
74
   * Definition file name shown in the title of the pane.
75
   */
76
  private final StringProperty mFilename = new SimpleStringProperty();
77
78
  private final TitledPane mTitledPane = new TitledPane();
79
80
  /**
81
   * Constructs a definition pane with a given tree view root.
82
   */
83
  public DefinitionPane() {
84
    final var treeView = getTreeView();
85
    treeView.setEditable( true );
86
    treeView.setCellFactory( cell -> createTreeCell() );
87
    treeView.setContextMenu( createContextMenu() );
88
    treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
89
    treeView.setShowRoot( false );
90
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
91
92
    final var bCreate = createButton(
93
        "create", TREE, e -> addItem() );
94
    final var bRename = createButton(
95
        "rename", EDIT, e -> editSelectedItem() );
96
    final var bDelete = createButton(
97
        "delete", TRASH, e -> deleteSelectedItems() );
98
99
    final var buttonBar = new HBox();
100
    buttonBar.getChildren().addAll( bCreate, bRename, bDelete );
101
    buttonBar.setAlignment( CENTER );
102
    buttonBar.setSpacing( 10 );
103
104
    final var titledPane = getTitledPane();
105
    titledPane.textProperty().bind( mFilename );
106
    titledPane.setContent( treeView );
107
    titledPane.setCollapsible( false );
108
    titledPane.setPadding( new Insets( 0, 0, 0, 0 ) );
109
110
    setTop( buttonBar );
111
    setCenter( titledPane );
112
    setAlignment( buttonBar, Pos.TOP_CENTER );
113
    setAlignment( titledPane, Pos.TOP_CENTER );
114
115
    titledPane.prefHeightProperty().bind( this.heightProperty() );
116
  }
117
118
  public void setTooltip( final Tooltip tooltip ) {
119
    getTitledPane().setTooltip( tooltip );
120
  }
121
122
  private TitledPane getTitledPane() {
123
    return mTitledPane;
124
  }
125
126
  private Button createButton(
127
      final String msgKey,
128
      final FontAwesomeIcon icon,
129
      final EventHandler<ActionEvent> eventHandler ) {
130
    final var keyPrefix = "Pane.definition.button." + msgKey;
131
    final var button = new Button( get( keyPrefix + ".label" ) );
132
    button.setOnAction( eventHandler );
133
134
    button.setGraphic(
135
        FontAwesomeIconFactory.get().createIcon( icon )
136
    );
137
    button.setTooltip( new Tooltip( get( keyPrefix + ".tooltip" ) ) );
138
139
    return button;
140
  }
141
142
  /**
143
   * Changes the root of the {@link TreeView} to the root of the
144
   * {@link TreeView} from the {@link DefinitionSource}.
145
   *
146
   * @param definitionSource Container for the hierarchy of key/value pairs
147
   *                         to replace the existing hierarchy.
148
   */
149
  public void update( final DefinitionSource definitionSource ) {
150
    assert definitionSource != null;
151
152
    final TreeAdapter treeAdapter = definitionSource.getTreeAdapter();
153
    final TreeItem<String> root = treeAdapter.adapt(
154
        get( "Pane.definition.node.root.title" )
155
    );
156
157
    getTreeView().setRoot( root );
158
  }
159
160
  public Map<String, String> toMap() {
161
    return TreeItemAdapter.toMap( getTreeView().getRoot() );
162
  }
163
164
  /**
165
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
166
   * is modified. The modifications include: item value changes, item additions,
167
   * and item removals.
168
   * <p>
169
   * Safe to call multiple times; if a handler is already registered, the
170
   * old handler is used.
171
   * </p>
172
   *
173
   * @param handler The handler to call whenever any {@link TreeItem} changes.
174
   */
175
  public void addTreeChangeHandler(
176
      final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
177
    final TreeItem<String> root = getTreeView().getRoot();
178
    root.addEventHandler( TreeItem.valueChangedEvent(), handler );
179
    root.addEventHandler( TreeItem.childrenModificationEvent(), handler );
180
  }
181
182
  public void addKeyEventHandler(
183
      final EventHandler<? super KeyEvent> handler ) {
184
    getKeyEventHandlers().add( handler );
185
  }
186
187
  /**
188
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
189
   * well-formed for export. A tree is considered well-formed if the following
190
   * conditions are met:
191
   *
192
   * <ul>
193
   *   <li>The root node contains at least one child node having a leaf.</li>
194
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
195
   * </ul>
196
   *
197
   * @return {@code null} if the document is well-formed, otherwise the
198
   * problematic child {@link TreeItem}.
199
   */
200
  public TreeItem<String> isTreeWellFormed() {
201
    final var root = getTreeView().getRoot();
202
203
    for( final var child : root.getChildren() ) {
204
      final var problemChild = isWellFormed( child );
205
206
      if( child.isLeaf() || problemChild != null ) {
207
        return problemChild;
208
      }
209
    }
210
211
    return null;
212
  }
213
214
  /**
215
   * Determines whether the document is well-formed by ensuring that
216
   * child branches do not contain multiple leaves.
217
   *
218
   * @param item The sub-tree to check for well-formedness.
219
   * @return {@code null} when the tree is well-formed, otherwise the
220
   * problematic {@link TreeItem}.
221
   */
222
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
223
    int childLeafs = 0;
224
    int childBranches = 0;
225
226
    for( final TreeItem<String> child : item.getChildren() ) {
227
      if( child.isLeaf() ) {
228
        childLeafs++;
229
      }
230
      else {
231
        childBranches++;
232
      }
233
234
      final var problemChild = isWellFormed( child );
235
236
      if( problemChild != null ) {
237
        return problemChild;
238
      }
239
    }
240
241
    return ((childBranches > 0 && childLeafs == 0) ||
242
        (childBranches == 0 && childLeafs <= 1)) ? null : item;
243
  }
244
245
  /**
246
   * Delegates to {@link VariableTreeItem#findLeafExact(String)}.
247
   *
248
   * @param text The value to find, never {@code null}.
249
   * @return The leaf that contains the given value, or {@code null} if
250
   * not found.
251
   */
252
  public VariableTreeItem<String> findLeafExact( final String text ) {
253
    return getTreeRoot().findLeafExact( text );
254
  }
255
256
  /**
257
   * Delegates to {@link VariableTreeItem#findLeafContains(String)}.
258
   *
259
   * @param text The value to find, never {@code null}.
260
   * @return The leaf that contains the given value, or {@code null} if
261
   * not found.
262
   */
263
  public VariableTreeItem<String> findLeafContains( final String text ) {
264
    return getTreeRoot().findLeafContains( text );
265
  }
266
267
  /**
268
   * Delegates to {@link VariableTreeItem#findLeafContains(String)}.
269
   *
270
   * @param text The value to find, never {@code null}.
271
   * @return The leaf that contains the given value, or {@code null} if
272
   * not found.
273
   */
274
  public VariableTreeItem<String> findLeafContainsNoCase( final String text ) {
275
    return getTreeRoot().findLeafContainsNoCase( text );
276
  }
277
278
  /**
279
   * Delegates to {@link VariableTreeItem#findLeafStartsWith(String)}.
280
   *
281
   * @param text The value to find, never {@code null}.
282
   * @return The leaf that contains the given value, or {@code null} if
283
   * not found.
284
   */
285
  public VariableTreeItem<String> findLeafStartsWith( final String text ) {
286
    return getTreeRoot().findLeafStartsWith( text );
287
  }
288
289
  /**
290
   * Expands the node to the root, recursively.
291
   *
292
   * @param <T>  The type of tree item to expand (usually String).
293
   * @param node The node to expand.
294
   */
295
  public <T> void expand( final TreeItem<T> node ) {
296
    if( node != null ) {
297
      expand( node.getParent() );
298
299
      if( !node.isLeaf() ) {
300
        node.setExpanded( true );
301
      }
302
    }
303
  }
304
305
  public void select( final TreeItem<String> item ) {
306
    getSelectionModel().clearSelection();
307
    getSelectionModel().select( getTreeView().getRow( item ) );
308
  }
309
310
  /**
311
   * Collapses the tree, recursively.
312
   */
313
  public void collapse() {
314
    collapse( getTreeRoot().getChildren() );
315
  }
316
317
  /**
318
   * Collapses the tree, recursively.
319
   *
320
   * @param <T>   The type of tree item to expand (usually String).
321
   * @param nodes The nodes to collapse.
322
   */
323
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
324
    for( final var node : nodes ) {
325
      node.setExpanded( false );
326
      collapse( node.getChildren() );
327
    }
328
  }
329
330
  /**
331
   * @return {@code true} when the user is editing a {@link TreeItem}.
332
   */
333
  private boolean isEditingTreeItem() {
334
    return getTreeView().editingItemProperty().getValue() != null;
335
  }
336
337
  /**
338
   * Changes to edit mode for the selected item.
339
   */
340
  private void editSelectedItem() {
341
    getTreeView().edit( getSelectedItem() );
342
  }
343
344
  /**
345
   * Removes all selected items from the {@link TreeView}.
346
   */
347
  private void deleteSelectedItems() {
348
    for( final var item : getSelectedItems() ) {
349
      final var parent = item.getParent();
350
351
      if( parent != null ) {
352
        parent.getChildren().remove( item );
353
      }
354
    }
355
  }
356
357
  /**
358
   * Deletes the selected item.
359
   */
360
  private void deleteSelectedItem() {
361
    final var c = getSelectedItem();
362
    getSiblings( c ).remove( c );
363
  }
364
365
  /**
366
   * Adds a new item under the selected item (or root if nothing is selected).
367
   * There are a few conditions to consider: when adding to the root,
368
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
369
   * root must contain two items: a key and a value.
370
   */
371
  private void addItem() {
372
    final var value = createTreeItem();
373
    getSelectedItem().getChildren().add( value );
374
    expand( value );
375
    select( value );
376
  }
377
378
  private ContextMenu createContextMenu() {
379
    final ContextMenu menu = new ContextMenu();
380
    final ObservableList<MenuItem> items = menu.getItems();
381
382
    addMenuItem( items, "Definition.menu.create" )
383
        .setOnAction( e -> addItem() );
384
385
    addMenuItem( items, "Definition.menu.rename" )
386
        .setOnAction( e -> editSelectedItem() );
387
388
    addMenuItem( items, "Definition.menu.remove" )
389
        .setOnAction( e -> deleteSelectedItem() );
390
391
    return menu;
392
  }
393
394
  /**
395
   * Executes hot-keys for edits to the definition tree.
396
   *
397
   * @param event Contains the key code of the key that was pressed.
398
   */
399
  private void keyEventFilter( final KeyEvent event ) {
400
    if( !isEditingTreeItem() ) {
401
      switch( event.getCode() ) {
402
        case ENTER:
403
          expand( getSelectedItem() );
404
          event.consume();
405
          break;
406
407
        case DELETE:
408
          deleteSelectedItems();
409
          break;
410
411
        case INSERT:
412
          addItem();
413
          break;
414
415
        case R:
416
          if( event.isControlDown() ) {
417
            editSelectedItem();
418
          }
419
420
          break;
421
      }
422
423
      for( final var handler : getKeyEventHandlers() ) {
424
        handler.handle( event );
425
      }
426
    }
427
  }
428
429
  /**
430
   * Adds a menu item to a list of menu items.
431
   *
432
   * @param items    The list of menu items to append to.
433
   * @param labelKey The resource bundle key name for the menu item's label.
434
   * @return The menu item added to the list of menu items.
435
   */
436
  private MenuItem addMenuItem(
437
      final List<MenuItem> items, final String labelKey ) {
438
    final MenuItem menuItem = createMenuItem( labelKey );
439
    items.add( menuItem );
440
    return menuItem;
441
  }
442
443
  private MenuItem createMenuItem( final String labelKey ) {
444
    return new MenuItem( get( labelKey ) );
445
  }
446
447
  private VariableTreeItem<String> createTreeItem() {
448
    return new VariableTreeItem<>( get( "Definition.menu.add.default" ) );
449
  }
450
451
  private TreeCell<String> createTreeCell() {
452
    return new TextFieldTreeCell<>( createStringConverter() ) {
453
      @Override
454
      public void commitEdit( final String newValue ) {
455
        super.commitEdit( newValue );
456
        select( getTreeItem() );
457
        requestFocus();
458
      }
459
    };
460
  }
461
462
  @Override
463
  public void requestFocus() {
464
    super.requestFocus();
465
    getTreeView().requestFocus();
466
  }
467
468
  private StringConverter<String> createStringConverter() {
469
    return new StringConverter<>() {
470
      @Override
471
      public String toString( final String object ) {
472
        return object == null ? "" : object;
473
      }
474
475
      @Override
476
      public String fromString( final String string ) {
477
        return string == null ? "" : string;
478
      }
479
    };
480
  }
481
482
  /**
483
   * Returns the tree view that contains the definition hierarchy.
484
   *
485
   * @return A non-null instance.
486
   */
487
  public TreeView<String> getTreeView() {
488
    return mTreeView;
489
  }
490
491
  /**
492
   * Returns this pane.
493
   *
494
   * @return this
495
   */
496
  public Node getNode() {
497
    return this;
498
  }
499
500
  /**
501
   * Returns the property used to set the title of the pane: the file name.
502
   *
503
   * @return A non-null property used for showing the definition file name.
504
   */
505
  public StringProperty filenameProperty() {
506
    return mFilename;
507
  }
508
509
  /**
510
   * Returns the root of the tree.
511
   *
512
   * @return The first node added to the definition tree.
513
   */
514
  private VariableTreeItem<String> getTreeRoot() {
515
    final TreeItem<String> root = getTreeView().getRoot();
516
517
    return root instanceof VariableTreeItem ?
518
        (VariableTreeItem<String>) root : new VariableTreeItem<>( "root" );
519
  }
520
521
  private ObservableList<TreeItem<String>> getSiblings(
522
      final TreeItem<String> item ) {
523
    final var root = getTreeView().getRoot();
524
    final var parent = (item == null || item == root) ? root : item.getParent();
525
526
    return parent.getChildren();
527
  }
528
529
  private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
530
    return getTreeView().getSelectionModel();
531
  }
532
533
  /**
534
   * Returns a copy of all the selected items.
535
   *
536
   * @return A list, possibly empty, containing all selected items in the
537
   * {@link TreeView}.
538
   */
539
  private List<TreeItem<String>> getSelectedItems() {
540
    return new ArrayList<>( getSelectionModel().getSelectedItems() );
541
  }
542
543
  public TreeItem<String> getSelectedItem() {
544
    final var item = getSelectionModel().getSelectedItem();
545
    return item == null ? getTreeView().getRoot() : item;
546
  }
547
548
  private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
549
    return mKeyEventHandlers;
550
  }
551
}
1552
A src/main/java/com/scrivenvar/definition/DefinitionSource.java
1
/*
2
 * Copyright 2020 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
/**
31
 * Represents behaviours for reading and writing string definitions. This
32
 * class cannot have any direct hooks into the user interface, as it defines
33
 * entry points into the definition data model loaded into an object
34
 * hierarchy. That hierarchy is converted to a UI model using an adapter
35
 * pattern.
36
 */
37
public interface DefinitionSource {
38
39
  /**
40
   * Creates an object capable of producing view-based objects from this
41
   * definition source.
42
   *
43
   * @return A hierarchical tree suitable for displaying in the definition pane.
44
   */
45
  TreeAdapter getTreeAdapter();
46
47
  /**
48
   * Returns the error message, if any, that occurred while loading the
49
   * definition source.
50
   *
51
   * @return The empty string if no error occurred, otherwise the error message.
52
   */
53
  default String getError() {
54
    return "";
55
  }
56
}
157
A src/main/java/com/scrivenvar/definition/DocumentParser.java
1
/*
2
 * Copyright 2020 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
/**
31
 * Responsible for parsing structured document formats.
32
 *
33
 * @param <T> The type of "node" for the document's object model.
34
 */
35
public interface DocumentParser<T> {
36
37
  /**
38
   * Parses a document into a nested object hierarchy. The object returned
39
   * from this call must be the root node in the document tree.
40
   *
41
   * @return The document's root node, which may be empty but never null.
42
   */
43
  T getDocumentRoot();
44
}
145
A src/main/java/com/scrivenvar/definition/MapInterpolator.java
1
/*
2
 * Copyright 2020 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 java.util.regex.Matcher;
32
import java.util.regex.Pattern;
33
34
/**
35
 * Responsible for performing string interpolation on key/value pairs stored
36
 * in a map. The values in the map can use a delimited syntax to refer to
37
 * keys in the map.
38
 */
39
public class MapInterpolator {
40
41
  /**
42
   * Matches variables delimited by dollar symbols.
43
   */
44
  private final static String REGEX = "(\\$.*?\\$)";
45
46
  /**
47
   * Compiled regular expression for matching delimited references.
48
   */
49
  private final static Pattern REGEX_PATTERN = Pattern.compile( REGEX );
50
51
  private final static int GROUP_DELIMITED = 1;
52
53
  /**
54
   * Empty.
55
   */
56
  private MapInterpolator() {
57
  }
58
59
  /**
60
   * Performs string interpolation on the values in the given map. This will
61
   * change any value in the map that contains a variable that matches
62
   * {@link #REGEX_PATTERN}.
63
   *
64
   * @param map Contains values that represent references to keys.
65
   */
66
  public static void interpolate( final Map<String, String> map ) {
67
    map.replaceAll( ( k, v ) -> resolve( map, v ) );
68
  }
69
70
  /**
71
   * Given a value with zero or more key references, this will resolve all
72
   * the values, recursively. If a key cannot be dereferenced, the value will
73
   * contain the key name.
74
   *
75
   * @param map   Map to search for keys when resolving key references.
76
   * @param value Value containing zero or more key references
77
   * @return The given value with all embedded key references interpolated.
78
   */
79
  private static String resolve(
80
      final Map<String, String> map, String value ) {
81
    final Matcher matcher = REGEX_PATTERN.matcher( value );
82
83
    while( matcher.find() ) {
84
      final String keyName = matcher.group( GROUP_DELIMITED );
85
      final String mapValue = map.get( keyName );
86
      final String keyValue = mapValue == null
87
          ? keyName
88
          : resolve( map, mapValue );
89
90
      value = value.replace( keyName, keyValue );
91
    }
92
93
    return value;
94
  }
95
}
196
A src/main/java/com/scrivenvar/definition/RootTreeItem.java
1
/*
2
 * Copyright 2020 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.TreeItem;
31
import javafx.scene.control.TreeView;
32
33
/**
34
 * Indicates that this is the top-most {@link TreeItem}. This class allows
35
 * the {@link TreeItemAdapter} to ignore the topmost definition. Such
36
 * contortions are necessary because {@link TreeView} requires a root item
37
 * that isn't part of the user's definition file.
38
 * <p>
39
 * Another approach would be to associate object pairs per {@link TreeItem},
40
 * but that would be a waste of memory since the only "exception" case is
41
 * the root {@link TreeItem}.
42
 * </p>
43
 *
44
 * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}.
45
 */
46
public class RootTreeItem<T> extends VariableTreeItem<T> {
47
  /**
48
   * Default constructor, calls the superclass, no other behaviour.
49
   *
50
   * @param value The {@link TreeItem} node name to construct the superclass.
51
   * @see TreeItemAdapter#toMap(TreeItem) for details on how this
52
   * class is used.
53
   */
54
  public RootTreeItem( final T value ) {
55
    super( value );
56
  }
57
}
158
A src/main/java/com/scrivenvar/definition/TreeAdapter.java
1
/*
2
 * Copyright 2020 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.TreeItem;
31
32
import java.io.IOException;
33
import java.nio.file.Path;
34
35
/**
36
 * Responsible for converting an object hierarchy into a {@link TreeItem}
37
 * hierarchy.
38
 */
39
public interface TreeAdapter {
40
  /**
41
   * Adapts the document produced by the given parser into a {@link TreeItem}
42
   * object that can be presented to the user within a GUI.
43
   *
44
   * @param root The default root node name.
45
   * @return The parsed document in a {@link TreeItem} that can be displayed
46
   * in a panel.
47
   */
48
  TreeItem<String> adapt( String root );
49
50
  /**
51
   * Exports the given root node to the given path.
52
   *
53
   * @param root The root node to export.
54
   * @param path Where to persist the data.
55
   * @throws IOException Could not write the data to the given path.
56
   */
57
  void export( TreeItem<String> root, Path path ) throws IOException;
58
}
159
A src/main/java/com/scrivenvar/definition/TreeItemAdapter.java
1
/*
2
 * Copyright 2020 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.fasterxml.jackson.databind.JsonNode;
31
import com.scrivenvar.decorators.YamlVariableDecorator;
32
import com.scrivenvar.preview.HTMLPreviewPane;
33
import javafx.scene.control.TreeItem;
34
import javafx.scene.control.TreeView;
35
36
import java.util.HashMap;
37
import java.util.Iterator;
38
import java.util.Map;
39
import java.util.Stack;
40
41
import static com.scrivenvar.Constants.DEFAULT_MAP_SIZE;
42
43
/**
44
 * Given a {@link TreeItem}, this will generate a flat map with all the
45
 * values in the tree recursively interpolated. The application integrates
46
 * definition files as follows:
47
 * <ol>
48
 *   <li>Load YAML file into {@link JsonNode} hierarchy.</li>
49
 *   <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
50
 *   <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li>
51
 *   <li>Substitute flat map variables into document as required.</li>
52
 * </ol>
53
 *
54
 * <p>
55
 * This class is responsible for producing the interpolated flat map. This
56
 * allows dynamic edits of the {@link TreeView} to be displayed in the
57
 * {@link HTMLPreviewPane} without having to reload the definition file.
58
 * Reloading the definition file would work, but has a number of drawbacks.
59
 * </p>
60
 */
61
public class TreeItemAdapter {
62
  /**
63
   * Separates YAML variable nodes (e.g., the dots in {@code $root.node.var$}).
64
   */
65
  public static final String SEPARATOR = ".";
66
67
  /**
68
   * Default buffer length for keys ({@link StringBuilder} has 16 character
69
   * buffer) that should be large enough for most keys to avoid reallocating
70
   * memory to increase the {@link StringBuilder}'s buffer.
71
   */
72
  public static final int DEFAULT_KEY_LENGTH = 64;
73
74
  /**
75
   * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
76
   * as a consecutive list.
77
   */
78
  private static final class TreeIterator
79
      implements Iterator<TreeItem<String>> {
80
    private final Stack<TreeItem<String>> mStack = new Stack<>();
81
82
    public TreeIterator( final TreeItem<String> root ) {
83
      if( root != null ) {
84
        mStack.push( root );
85
      }
86
    }
87
88
    @Override
89
    public boolean hasNext() {
90
      return !mStack.isEmpty();
91
    }
92
93
    @Override
94
    public TreeItem<String> next() {
95
      final TreeItem<String> next = mStack.pop();
96
      next.getChildren().forEach( mStack::push );
97
98
      return next;
99
    }
100
  }
101
102
  private TreeItemAdapter() {
103
  }
104
105
  /**
106
   * Iterate over a given root node (at any level of the tree) and process each
107
   * leaf node into a flat map. Values must be interpolated separately.
108
   */
109
  public static Map<String, String> toMap( final TreeItem<String> root ) {
110
    final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE );
111
    final TreeIterator iterator = new TreeIterator( root );
112
113
    iterator.forEachRemaining( item -> {
114
      if( item.isLeaf() ) {
115
        map.put( toPath( item.getParent() ), item.getValue() );
116
      }
117
    } );
118
119
    return map;
120
  }
121
122
123
  /**
124
   * For a given node, this will ascend the tree to generate a key name
125
   * that is associated with the leaf node's value.
126
   *
127
   * @param node Ascendants represent the key to this node's value.
128
   * @param <T>  Data type that the {@link TreeItem} contains.
129
   * @return The string representation of the node's unique key.
130
   */
131
  public static <T> String toPath( TreeItem<T> node ) {
132
    assert node != null;
133
134
    final StringBuilder key = new StringBuilder( DEFAULT_KEY_LENGTH );
135
    final Stack<TreeItem<T>> stack = new Stack<>();
136
137
    while( node != null && !(node instanceof RootTreeItem) ) {
138
      stack.push( node );
139
      node = node.getParent();
140
    }
141
142
    // Gets set at end of first iteration (to avoid an if condition).
143
    String separator = "";
144
145
    while( !stack.empty() ) {
146
      final T subkey = stack.pop().getValue();
147
      key.append( separator );
148
      key.append( subkey );
149
      separator = SEPARATOR;
150
    }
151
152
    return YamlVariableDecorator.entoken( key.toString() );
153
  }
154
}
1155
A src/main/java/com/scrivenvar/definition/VariableTreeItem.java
1
/*
2
 * Copyright 2020 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.TreeItem;
31
32
import java.text.Normalizer;
33
import java.util.Stack;
34
import java.util.function.BiFunction;
35
36
import static java.text.Normalizer.Form.NFD;
37
38
/**
39
 * Provides behaviour afforded to variable names and their corresponding value.
40
 *
41
 * @param <T> The type of TreeItem (usually String).
42
 */
43
public class VariableTreeItem<T> extends TreeItem<T> {
44
45
  /**
46
   * Constructs a new item with a default value.
47
   *
48
   * @param value Passed up to superclass.
49
   */
50
  public VariableTreeItem( final T value ) {
51
    super( value );
52
  }
53
54
  /**
55
   * Finds a leaf starting at the current node with text that matches the given
56
   * value. Search is performed case-sensitively.
57
   *
58
   * @param text The text to match against each leaf in the tree.
59
   * @return The leaf that has a value exactly matching the given text.
60
   */
61
  public VariableTreeItem<T> findLeafExact( final String text ) {
62
    return findLeaf( text, VariableTreeItem::valueEquals );
63
  }
64
65
  /**
66
   * Finds a leaf starting at the current node with text that matches the given
67
   * value. Search is performed case-sensitively.
68
   *
69
   * @param text The text to match against each leaf in the tree.
70
   * @return The leaf that has a value that contains the given text.
71
   */
72
  public VariableTreeItem<T> findLeafContains( final String text ) {
73
    return findLeaf( text, VariableTreeItem::valueContains );
74
  }
75
76
  /**
77
   * Finds a leaf starting at the current node with text that matches the given
78
   * value. Search is performed case-insensitively.
79
   *
80
   * @param text The text to match against each leaf in the tree.
81
   * @return The leaf that has a value that contains the given text.
82
   */
83
  public VariableTreeItem<T> findLeafContainsNoCase( final String text ) {
84
    return findLeaf( text, VariableTreeItem::valueContainsNoCase );
85
  }
86
87
  /**
88
   * Finds a leaf starting at the current node with text that matches the given
89
   * value. Search is performed case-sensitively.
90
   *
91
   * @param text The text to match against each leaf in the tree.
92
   * @return The leaf that has a value that starts with the given text.
93
   */
94
  public VariableTreeItem<T> findLeafStartsWith( final String text ) {
95
    return findLeaf( text, VariableTreeItem::valueStartsWith );
96
  }
97
98
  /**
99
   * Finds a leaf starting at the current node with text that matches the given
100
   * value.
101
   *
102
   * @param text     The text to match against each leaf in the tree.
103
   * @param findMode What algorithm is used to match the given text.
104
   * @return The leaf that has a value starting with the given text, or {@code
105
   * null} if there was no match found.
106
   */
107
  public VariableTreeItem<T> findLeaf(
108
      final String text,
109
      final BiFunction<VariableTreeItem<T>, String, Boolean> findMode ) {
110
    final Stack<VariableTreeItem<T>> stack = new Stack<>();
111
    stack.push( this );
112
113
    // Don't hunt for blank (empty) keys.
114
    boolean found = text.isBlank();
115
116
    while( !found && !stack.isEmpty() ) {
117
      final VariableTreeItem<T> node = stack.pop();
118
119
      for( final TreeItem<T> child : node.getChildren() ) {
120
        final VariableTreeItem<T> result = (VariableTreeItem<T>) child;
121
122
        if( result.isLeaf() ) {
123
          if( found = findMode.apply( result, text ) ) {
124
            return result;
125
          }
126
        }
127
        else {
128
          stack.push( result );
129
        }
130
      }
131
    }
132
133
    return null;
134
  }
135
136
  /**
137
   * Returns the value of the string without diacritic marks.
138
   *
139
   * @return A non-null, possibly empty string.
140
   */
141
  private String getDiacriticlessValue() {
142
    return Normalizer.normalize( getValue().toString(), NFD )
143
                     .replaceAll( "\\p{M}", "" );
144
  }
145
146
  /**
147
   * Returns true if this node is a leaf and its value equals the given text.
148
   *
149
   * @param s The text to compare against the node value.
150
   * @return true Node is a leaf and its value equals the given value.
151
   */
152
  private boolean valueEquals( final String s ) {
153
    return isLeaf() && getValue().equals( s );
154
  }
155
156
  /**
157
   * Returns true if this node is a leaf and its value contains the given text.
158
   *
159
   * @param s The text to compare against the node value.
160
   * @return true Node is a leaf and its value contains the given value.
161
   */
162
  private boolean valueContains( final String s ) {
163
    return isLeaf() && getDiacriticlessValue().contains( s );
164
  }
165
166
  /**
167
   * Returns true if this node is a leaf and its value contains the given text.
168
   *
169
   * @param s The text to compare against the node value.
170
   * @return true Node is a leaf and its value contains the given value.
171
   */
172
  private boolean valueContainsNoCase( final String s ) {
173
    return isLeaf() && getDiacriticlessValue()
174
        .toLowerCase()
175
        .contains( s.toLowerCase() );
176
  }
177
178
  /**
179
   * Returns true if this node is a leaf and its value starts with the given
180
   * text.
181
   *
182
   * @param s The text to compare against the node value.
183
   * @return true Node is a leaf and its value starts with the given value.
184
   */
185
  private boolean valueStartsWith( final String s ) {
186
    return isLeaf() && getDiacriticlessValue().startsWith( s );
187
  }
188
189
  /**
190
   * Returns the path for this node, with nodes made distinct using the
191
   * separator character. This uses two loops: one for pushing nodes onto a
192
   * stack and one for popping them off to create the path in desired order.
193
   *
194
   * @return A non-null string, possibly empty.
195
   */
196
  public String toPath() {
197
    return TreeItemAdapter.toPath( getParent() );
198
  }
199
}
1200
A src/main/java/com/scrivenvar/definition/yaml/YamlDefinitionSource.java
1
/*
2
 * Copyright 2020 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.scrivenvar.definition.DefinitionSource;
31
import com.scrivenvar.definition.TreeAdapter;
32
33
import java.nio.file.Path;
34
35
/**
36
 * Represents a definition data source for YAML files.
37
 */
38
public class YamlDefinitionSource implements DefinitionSource {
39
40
  private final YamlTreeAdapter mYamlTreeAdapter;
41
42
  /**
43
   * Constructs a new YAML definition source, populated from the given file.
44
   *
45
   * @param path Path to the YAML definition file.
46
   */
47
  public YamlDefinitionSource( final Path path ) {
48
    assert path != null;
49
50
    mYamlTreeAdapter = new YamlTreeAdapter( path );
51
  }
52
53
  @Override
54
  public TreeAdapter getTreeAdapter() {
55
    return mYamlTreeAdapter;
56
  }
57
58
  @Override
59
  public String getError() {
60
    return getYamlParser().getError();
61
  }
62
63
  private YamlParser getYamlParser() {
64
    return mYamlTreeAdapter.getYamlParser();
65
  }
66
}
167
A src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
1
/*
2
 * Copyright 2020 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.fasterxml.jackson.databind.ObjectMapper;
32
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
33
import com.scrivenvar.Messages;
34
import com.scrivenvar.definition.DocumentParser;
35
36
import java.io.InputStream;
37
import java.nio.file.Files;
38
import java.nio.file.Path;
39
40
import static com.scrivenvar.Constants.STATUS_BAR_OK;
41
42
/**
43
 * Responsible for reading a YAML document into an object hierarchy.
44
 */
45
public class YamlParser implements DocumentParser<JsonNode> {
46
47
  /**
48
   * Error that occurred while parsing.
49
   */
50
  private String mError;
51
52
  /**
53
   * Start of the Universe (the YAML document node that contains all others).
54
   */
55
  private final JsonNode mDocumentRoot;
56
57
  /**
58
   * Creates a new YamlParser instance that attempts to parse the contents
59
   * of the YAML document given from a path. In the event that the file either
60
   * does not exist or is empty, a fake
61
   *
62
   * @param path Path to a file containing YAML data to parse.
63
   */
64
  public YamlParser( final Path path ) {
65
    assert path != null;
66
    mDocumentRoot = parse( path );
67
  }
68
69
  /**
70
   * Returns the parent node for the entire YAML document tree.
71
   *
72
   * @return The document root, never {@code null}.
73
   */
74
  @Override
75
  public JsonNode getDocumentRoot() {
76
    return mDocumentRoot;
77
  }
78
79
  /**
80
   * Parses the given path containing YAML data into an object hierarchy.
81
   *
82
   * @param path {@link Path} to the YAML resource to parse.
83
   * @return The parsed contents, or an empty object hierarchy.
84
   */
85
  private JsonNode parse( final Path path ) {
86
    try( final InputStream in = Files.newInputStream( path ) ) {
87
      setError( Messages.get( STATUS_BAR_OK ) );
88
89
      return new ObjectMapper( new YAMLFactory() ).readTree( in );
90
    } catch( final Exception e ) {
91
      setError( Messages.get( "yaml.error.open" ) );
92
93
      // Ensure that a document root node exists by relying on the
94
      // default failure condition when processing. This is required
95
      // because the input stream could not be read.
96
      return new ObjectMapper().createObjectNode();
97
    }
98
  }
99
100
  private void setError( final String error ) {
101
    mError = error;
102
  }
103
104
  /**
105
   * Returns the last error message, if any, that occurred during parsing.
106
   *
107
   * @return The error message or the empty string if no error occurred.
108
   */
109
  public String getError() {
110
    return mError;
111
  }
112
}
1113
A src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java
1
/*
2
 * Copyright 2020 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.fasterxml.jackson.databind.node.ObjectNode;
32
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
33
import com.scrivenvar.definition.RootTreeItem;
34
import com.scrivenvar.definition.TreeAdapter;
35
import com.scrivenvar.definition.VariableTreeItem;
36
import javafx.scene.control.TreeItem;
37
import javafx.scene.control.TreeView;
38
39
import java.io.IOException;
40
import java.nio.file.Path;
41
import java.util.Map.Entry;
42
43
/**
44
 * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
45
 * interface and vice-versa.
46
 */
47
public class YamlTreeAdapter implements TreeAdapter {
48
  private final YamlParser mParser;
49
50
  /**
51
   * Constructs a new instance that will use the given path to read
52
   * the object hierarchy from a data source.
53
   *
54
   * @param path Path to YAML contents to parse.
55
   */
56
  public YamlTreeAdapter( final Path path ) {
57
    mParser = new YamlParser( path );
58
  }
59
60
  @Override
61
  public void export( final TreeItem<String> treeItem, final Path path )
62
      throws IOException {
63
    final YAMLMapper mapper = new YAMLMapper();
64
    final ObjectNode root = mapper.createObjectNode();
65
66
    // Iterate over the root item's children. The root item is used by the
67
    // application to ensure definitions can always be added to a tree, as
68
    // such it is not meant to be exported, only its children.
69
    for( final TreeItem<String> child : treeItem.getChildren() ) {
70
      export( child, root );
71
    }
72
73
    // Writes as UTF8 by default.
74
    mapper.writeValue( path.toFile(), root );
75
  }
76
77
  /**
78
   * Recursive method to generate an object hierarchy that represents the
79
   * given {@link TreeItem} hierarchy.
80
   *
81
   * @param item The {@link TreeItem} to reproduce as an object hierarchy.
82
   * @param node The {@link ObjectNode} to update to reflect the
83
   *             {@link TreeItem} hierarchy.
84
   */
85
  private void export( final TreeItem<String> item, ObjectNode node ) {
86
    final var children = item.getChildren();
87
88
    // If the current item has more than one non-leaf child, it's an
89
    // object node and must become a new nested object.
90
    if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) {
91
      node = node.putObject( item.getValue() );
92
    }
93
94
    for( final TreeItem<String> child : children ) {
95
      if( child.isLeaf() ) {
96
        node.put( item.getValue(), child.getValue() );
97
      }
98
      else {
99
        export( child, node );
100
      }
101
    }
102
  }
103
104
  /**
105
   * Converts a YAML document to a {@link TreeItem} based on the document
106
   * keys. Only the first document in the stream is adapted.
107
   *
108
   * @param root Root {@link TreeItem} node name.
109
   * @return A {@link TreeItem} populated with all the keys in the YAML
110
   * document.
111
   */
112
  public TreeItem<String> adapt( final String root ) {
113
    final JsonNode rootNode = getYamlParser().getDocumentRoot();
114
    final TreeItem<String> rootItem = createRootTreeItem( root );
115
116
    rootItem.setExpanded( true );
117
    adapt( rootNode, rootItem );
118
    return rootItem;
119
  }
120
121
  /**
122
   * Iterate over a given root node (at any level of the tree) and adapt each
123
   * leaf node.
124
   *
125
   * @param rootNode A JSON node (YAML node) to adapt.
126
   * @param rootItem The tree item to use as the root when processing the node.
127
   */
128
  private void adapt(
129
      final JsonNode rootNode, final TreeItem<String> rootItem ) {
130
    rootNode.fields().forEachRemaining(
131
        ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem )
132
    );
133
  }
134
135
  /**
136
   * Recursively adapt each rootNode to a corresponding rootItem.
137
   *
138
   * @param rootNode The node to adapt.
139
   * @param rootItem The item to adapt using the node's key.
140
   */
141
  private void adapt(
142
      final Entry<String, JsonNode> rootNode,
143
      final TreeItem<String> rootItem ) {
144
    final JsonNode leafNode = rootNode.getValue();
145
    final String key = rootNode.getKey();
146
    final TreeItem<String> leaf = createTreeItem( key );
147
148
    if( leafNode.isValueNode() ) {
149
      leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) );
150
    }
151
152
    rootItem.getChildren().add( leaf );
153
154
    if( leafNode.isObject() ) {
155
      adapt( leafNode, leaf );
156
    }
157
  }
158
159
  /**
160
   * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
161
   *
162
   * @param value The node's value.
163
   * @return A new {@link TreeItem}, never {@code null}.
164
   */
165
  private TreeItem<String> createTreeItem( final String value ) {
166
    return new VariableTreeItem<>( value );
167
  }
168
169
  /**
170
   * Creates a new {@link TreeItem} that is intended to be the root-level item
171
   * added to the {@link TreeView}. This allows the root item to be
172
   * distinguished from the other items so that reference keys do not include
173
   * "Definition" as part of their name.
174
   *
175
   * @param value The node's value.
176
   * @return A new {@link TreeItem}, never {@code null}.
177
   */
178
  private TreeItem<String> createRootTreeItem( final String value ) {
179
    return new RootTreeItem<>( value );
180
  }
181
182
  public YamlParser getYamlParser() {
183
    return mParser;
184
  }
185
}
1186
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
 * @param <T> The type of dialog to create (usually String).
41
 */
42
public abstract class AbstractDialog<T> extends Dialog<T> {
43
44
  /**
45
   * Ensures that all dialogs can be closed.
46
   *
47
   * @param owner The parent window of this dialog.
48
   * @param title The messages title to display in the title bar.
49
   */
50
  @SuppressWarnings( "OverridableMethodCallInConstructor" )
51
  public AbstractDialog( final Window owner, final String title ) {
52
    setTitle( get( title ) );
53
    setResizable( true );
54
55
    initOwner( owner );
56
    initCloseAction();
57
    initDialogPane();
58
    initDialogButtons();
59
    initComponents();
60
  }
61
62
  /**
63
   * Initialize the component layout.
64
   */
65
  protected abstract void initComponents();
66
67
  /**
68
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
69
   */
70
  protected void initDialogPane() {
71
    setDialogPane( new ButtonOrderPane() );
72
  }
73
  
74
  /**
75
   * Set an OK and CANCEL button on the dialog.
76
   */
77
  protected void initDialogButtons() {
78
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
79
  }
80
81
  /**
82
   * Attaches a setOnCloseRequest to the dialog's [X] button so that the user
83
   * can always close the window, even if there's an error.
84
   */
85
  protected final void initCloseAction() {
86
    final Window window = getDialogPane().getScene().getWindow();
87
    window.setOnCloseRequest( event -> window.hide() );
88
  }
89
}
190
A src/main/java/com/scrivenvar/dialogs/ImageDialog.java
1
/*
2
 * Copyright 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
public class ImageDialog extends AbstractDialog<String> {
49
50
  private final StringProperty image = new SimpleStringProperty();
51
52
  public ImageDialog( final Window owner, final Path basePath ) {
53
    super(owner, "Dialog.image.title" );
54
    
55
    final DialogPane dialogPane = getDialogPane();
56
    dialogPane.setContent( pane );
57
58
    linkBrowseFileButton.setBasePath( basePath );
59
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
60
    linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
61
62
    dialogPane.lookupButton( OK ).disableProperty().bind(
63
      urlField.escapedTextProperty().isEmpty()
64
      .or( textField.escapedTextProperty().isEmpty() ) );
65
66
    image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
69
    previewField.textProperty().bind( image );
70
71
    setResultConverter( dialogButton -> {
72
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
73
      return (data == ButtonData.OK_DONE) ? image.get() : null;
74
    } );
75
76
    Platform.runLater( () -> {
77
      urlField.requestFocus();
78
79
      if( urlField.getText().startsWith( "http://" ) ) {
80
        urlField.selectRange( "http://".length(), urlField.getLength() );
81
      }
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
    linkBrowseFileButton = new BrowseFileButton();
92
    Label textLabel = new Label();
93
    textField = new EscapeTextField();
94
    Label titleLabel = new Label();
95
    titleField = new EscapeTextField();
96
    Label previewLabel = new Label();
97
    previewField = new Label();
98
99
    //======== pane ========
100
    {
101
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
102
      pane.setRows( "[][][][]" );
103
104
      //---- urlLabel ----
105
      urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
106
      pane.add( urlLabel, "cell 0 0" );
107
108
      //---- urlField ----
109
      urlField.setEscapeCharacters( "()" );
110
      urlField.setText( "http://yourlink.com" );
111
      urlField.setPromptText( "http://yourlink.com" );
112
      pane.add( urlField, "cell 1 0" );
113
      pane.add( linkBrowseFileButton, "cell 2 0" );
114
115
      //---- textLabel ----
116
      textLabel.setText( get( "Dialog.image.textLabel.text" ) );
117
      pane.add( textLabel, "cell 0 1" );
118
119
      //---- textField ----
120
      textField.setEscapeCharacters( "[]" );
121
      pane.add( textField, "cell 1 1 2 1" );
122
123
      //---- titleLabel ----
124
      titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
125
      pane.add( titleLabel, "cell 0 2" );
126
      pane.add( titleField, "cell 1 2 2 1" );
127
128
      //---- previewLabel ----
129
      previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
130
      pane.add( previewLabel, "cell 0 3" );
131
      pane.add( previewField, "cell 1 3 2 1" );
132
    }
133
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
134
  }
135
136
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
137
  private MigPane pane;
138
  private EscapeTextField urlField;
139
  private BrowseFileButton linkBrowseFileButton;
140
  private EscapeTextField textField;
141
  private EscapeTextField titleField;
142
  private Label previewField;
143
	// JFormDesigner - End of variables declaration  //GEN-END:variables
144
}
1145
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 com.scrivenvar.controls.EscapeTextField;
31
import com.scrivenvar.editors.markdown.HyperlinkModel;
32
import javafx.application.Platform;
33
import javafx.beans.binding.Bindings;
34
import javafx.beans.property.SimpleStringProperty;
35
import javafx.beans.property.StringProperty;
36
import javafx.scene.control.ButtonBar.ButtonData;
37
import javafx.scene.control.DialogPane;
38
import javafx.scene.control.Label;
39
import javafx.stage.Window;
40
import org.tbee.javafx.scene.layout.fxml.MigPane;
41
42
import static com.scrivenvar.Messages.get;
43
import static javafx.scene.control.ButtonType.OK;
44
45
/**
46
 * Dialog to enter a markdown link.
47
 */
48
public class LinkDialog extends AbstractDialog<String> {
49
50
  private final StringProperty link = new SimpleStringProperty();
51
52
  public LinkDialog(
53
    final Window owner, final HyperlinkModel hyperlink ) {
54
    super( owner, "Dialog.link.title" );
55
56
    final DialogPane dialogPane = getDialogPane();
57
    dialogPane.setContent( pane );
58
59
    dialogPane.lookupButton( OK ).disableProperty().bind(
60
      urlField.escapedTextProperty().isEmpty() );
61
62
    textField.setText( hyperlink.getText() );
63
    urlField.setText( hyperlink.getUrl() );
64
    titleField.setText( hyperlink.getTitle() );
65
66
    link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
67
      .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
68
      .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
69
        .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
70
        .otherwise( urlField.escapedTextProperty() ) ) );
71
72
    setResultConverter( dialogButton -> {
73
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
74
      return (data == ButtonData.OK_DONE) ? link.get() : null;
75
    } );
76
77
    Platform.runLater( () -> {
78
      urlField.requestFocus();
79
      urlField.selectRange( 0, urlField.getLength() );
80
    } );
81
  }
82
83
  @Override
84
  protected void initComponents() {
85
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
86
    pane = new MigPane();
87
    Label urlLabel = new Label();
88
    urlField = new EscapeTextField();
89
    Label textLabel = new Label();
90
    textField = new EscapeTextField();
91
    Label titleLabel = new Label();
92
    titleField = new EscapeTextField();
93
94
    //======== pane ========
95
    {
96
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
97
      pane.setRows( "[][][][]" );
98
99
      //---- urlLabel ----
100
      urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
101
      pane.add( urlLabel, "cell 0 0" );
102
103
      //---- urlField ----
104
      urlField.setEscapeCharacters( "()" );
105
      pane.add( urlField, "cell 1 0" );
106
107
      //---- textLabel ----
108
      textLabel.setText( get( "Dialog.link.textLabel.text" ) );
109
      pane.add( textLabel, "cell 0 1" );
110
111
      //---- textField ----
112
      textField.setEscapeCharacters( "[]" );
113
      pane.add( textField, "cell 1 1 3 1" );
114
115
      //---- titleLabel ----
116
      titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
117
      pane.add( titleLabel, "cell 0 2" );
118
      pane.add( titleField, "cell 1 2 3 1" );
119
    }
120
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
121
  }
122
123
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
124
  private MigPane pane;
125
  private EscapeTextField urlField;
126
  private EscapeTextField textField;
127
  private EscapeTextField titleField;
128
  // JFormDesigner - End of variables declaration  //GEN-END:variables
129
}
1130
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/editors/EditorPane.java
1
/*
2
 * Copyright 2020 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 javafx.beans.property.ObjectProperty;
31
import javafx.beans.property.SimpleObjectProperty;
32
import javafx.beans.value.ChangeListener;
33
import javafx.event.Event;
34
import javafx.scene.control.ScrollPane;
35
import javafx.scene.layout.Pane;
36
import org.fxmisc.flowless.VirtualizedScrollPane;
37
import org.fxmisc.richtext.StyleClassedTextArea;
38
import org.fxmisc.undo.UndoManager;
39
import org.fxmisc.wellbehaved.event.EventPattern;
40
import org.fxmisc.wellbehaved.event.Nodes;
41
42
import java.nio.file.Path;
43
import java.util.function.Consumer;
44
45
import static javafx.application.Platform.runLater;
46
import static org.fxmisc.wellbehaved.event.InputMap.consume;
47
48
/**
49
 * Represents common editing features for various types of text editors.
50
 */
51
public class EditorPane extends Pane {
52
53
  private final StyleClassedTextArea mEditor =
54
      new StyleClassedTextArea( false );
55
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
56
      new VirtualizedScrollPane<>( mEditor );
57
  private final ObjectProperty<Path> mPath = new SimpleObjectProperty<>();
58
59
  public EditorPane() {
60
    getScrollPane().setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
61
  }
62
63
  @Override
64
  public void requestFocus() {
65
    requestFocus( 3 );
66
  }
67
68
  /**
69
   * There's a race-condition between displaying the {@link EditorPane}
70
   * and giving the {@link #mEditor} focus. Try to focus up to {@code max}
71
   * times before giving up.
72
   *
73
   * @param max The number of attempts to try to request focus.
74
   */
75
  private void requestFocus( final int max ) {
76
    if( max > 0 ) {
77
      runLater(
78
          () -> {
79
            final var editor = getEditor();
80
81
            if( !editor.isFocused() ) {
82
              editor.requestFocus();
83
              requestFocus( max - 1 );
84
            }
85
          }
86
      );
87
    }
88
  }
89
90
  public void undo() {
91
    getUndoManager().undo();
92
  }
93
94
  public void redo() {
95
    getUndoManager().redo();
96
  }
97
98
  /**
99
   * Cuts the actively selected text; if no text is selected, this will cut
100
   * the entire paragraph.
101
   */
102
  public void cut() {
103
    final var editor = getEditor();
104
    final var selected = editor.getSelectedText();
105
106
    if( selected == null || selected.isEmpty() ) {
107
      editor.selectParagraph();
108
    }
109
110
    editor.cut();
111
  }
112
113
  public void copy() {
114
    getEditor().copy();
115
  }
116
117
  public void paste() {
118
    getEditor().paste();
119
  }
120
121
  public void selectAll() {
122
    getEditor().selectAll();
123
  }
124
125
  public UndoManager<?> getUndoManager() {
126
    return getEditor().getUndoManager();
127
  }
128
129
  public String getText() {
130
    return getEditor().getText();
131
  }
132
133
  public void setText( final String text ) {
134
    final var editor = getEditor();
135
    editor.deselect();
136
    editor.replaceText( text );
137
    getUndoManager().mark();
138
  }
139
140
  /**
141
   * Call to hook into changes to the text area.
142
   *
143
   * @param listener Receives editor text change events.
144
   */
145
  public void addTextChangeListener(
146
      final ChangeListener<? super String> listener ) {
147
    getEditor().textProperty().addListener( listener );
148
  }
149
150
  /**
151
   * Notifies observers when the caret changes paragraph.
152
   *
153
   * @param listener Receives change event.
154
   */
155
  public void addCaretParagraphListener(
156
      final ChangeListener<? super Integer> listener ) {
157
    getEditor().currentParagraphProperty().addListener( listener );
158
  }
159
160
  /**
161
   * Notifies observers when the caret changes position.
162
   *
163
   * @param listener Receives change event.
164
   */
165
  public void addCaretPositionListener(
166
      final ChangeListener<? super Integer> listener ) {
167
    getEditor().caretPositionProperty().addListener( listener );
168
  }
169
170
  /**
171
   * This method adds listeners to editor events.
172
   *
173
   * @param <T>      The event type.
174
   * @param <U>      The consumer type for the given event type.
175
   * @param event    The event of interest.
176
   * @param consumer The method to call when the event happens.
177
   */
178
  public <T extends Event, U extends T> void addKeyboardListener(
179
      final EventPattern<? super T, ? extends U> event,
180
      final Consumer<? super U> consumer ) {
181
    Nodes.addInputMap( getEditor(), consume( event, consumer ) );
182
  }
183
184
  /**
185
   * Repositions the cursor and scroll bar to the top of the file.
186
   */
187
  public void scrollToTop() {
188
    getEditor().moveTo( 0 );
189
    getScrollPane().scrollYToPixel( 0 );
190
  }
191
192
  public StyleClassedTextArea getEditor() {
193
    return mEditor;
194
  }
195
196
  /**
197
   * Returns the scroll pane that contains the text area.
198
   *
199
   * @return The scroll pane that contains the content to edit.
200
   */
201
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
202
    return mScrollPane;
203
  }
204
205
  public Path getPath() {
206
    return mPath.get();
207
  }
208
209
  public void setPath( final Path path ) {
210
    mPath.set( path );
211
  }
212
}
1213
A src/main/java/com/scrivenvar/editors/VariableNameDecoratorFactory.java
1
/*
2
 * Copyright 2020 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
public class VariableNameDecoratorFactory extends AbstractFileFactory {
41
42
  private VariableNameDecoratorFactory() {
43
  }
44
45
  public static VariableDecorator newInstance( final Path path ) {
46
    final var factory = new VariableNameDecoratorFactory();
47
    final VariableDecorator result;
48
49
    switch( factory.lookup( path ) ) {
50
      case RMARKDOWN:
51
      case RXML:
52
        result = new RVariableDecorator();
53
        break;
54
55
      default:
56
        result = new YamlVariableDecorator();
57
        break;
58
    }
59
60
    return result;
61
  }
62
}
163
A src/main/java/com/scrivenvar/editors/VariableNameInjector.java
1
/*
2
 * Copyright 2020 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.decorators.VariableDecorator;
32
import com.scrivenvar.definition.DefinitionPane;
33
import com.scrivenvar.definition.VariableTreeItem;
34
import javafx.scene.control.TreeItem;
35
import javafx.scene.input.KeyEvent;
36
import org.fxmisc.richtext.StyledTextArea;
37
38
import java.nio.file.Path;
39
import java.text.BreakIterator;
40
41
import static javafx.scene.input.KeyCode.SPACE;
42
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
43
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
44
45
/**
46
 * Provides the logic for injecting variable names within the editor.
47
 */
48
public final class VariableNameInjector {
49
50
  /**
51
   * Recipient of name injections.
52
   */
53
  private FileEditorTab mTab;
54
55
  /**
56
   * Initiates double-click events.
57
   */
58
  private final DefinitionPane mDefinitionPane;
59
60
  /**
61
   * Initializes the variable name injector against the given pane.
62
   *
63
   * @param pane The definition panel to listen to for double-click events.
64
   */
65
  public VariableNameInjector( final DefinitionPane pane ) {
66
    mDefinitionPane = pane;
67
  }
68
69
  /**
70
   * Trap Control+Space.
71
   *
72
   * @param tab Editor where variable names get injected.
73
   */
74
  public void addListener( final FileEditorTab tab ) {
75
    assert tab != null;
76
    mTab = tab;
77
78
    tab.getEditorPane().addKeyboardListener(
79
        keyPressed( SPACE, CONTROL_DOWN ),
80
        this::autoinsert
81
    );
82
  }
83
84
  /**
85
   * Inserts the currently selected variable from the {@link DefinitionPane}.
86
   */
87
  public void injectSelectedItem() {
88
    final var pane = getDefinitionPane();
89
    final TreeItem<String> item = pane.getSelectedItem();
90
91
    if( item.isLeaf() ) {
92
      final var leaf = pane.findLeafExact( item.getValue() );
93
      final var editor = getEditor();
94
95
      editor.insertText( editor.getCaretPosition(), decorate( leaf ) );
96
    }
97
  }
98
99
  /**
100
   * Pressing Control+SPACE will find a node that matches the current word and
101
   * substitute the YAML variable reference.
102
   *
103
   * @param e Ignored -- it can only be Control+SPACE.
104
   */
105
  private void autoinsert( final KeyEvent e ) {
106
    final String paragraph = getCaretParagraph();
107
    final int[] boundaries = getWordBoundariesAtCaret();
108
    final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
109
    final VariableTreeItem<String> leaf = findLeaf( word );
110
111
    if( leaf != null ) {
112
      replaceText( boundaries[ 0 ], boundaries[ 1 ], decorate( leaf ) );
113
      expand( leaf );
114
    }
115
  }
116
117
  private int[] getWordBoundariesAtCaret() {
118
    final String paragraph = getCaretParagraph();
119
    int offset = getCurrentCaretColumn();
120
121
    final BreakIterator wordBreaks = BreakIterator.getWordInstance();
122
    wordBreaks.setText( paragraph );
123
124
    // Scan back until the first word is found.
125
    while( offset > 0 && wordBreaks.isBoundary( offset ) ) {
126
      offset--;
127
    }
128
129
    final int[] boundaries = new int[ 2 ];
130
    boundaries[ 1 ] = wordBreaks.following( offset );
131
    boundaries[ 0 ] = wordBreaks.previous();
132
133
    return boundaries;
134
  }
135
136
  /**
137
   * Decorates a {@link TreeItem} using the syntax specific to the type of
138
   * document being edited.
139
   *
140
   * @param leaf The path to the leaf (the definition key) to be decorated.
141
   */
142
  private String decorate( final VariableTreeItem<String> leaf ) {
143
    return decorate( leaf.toPath() );
144
  }
145
146
  /**
147
   * Decorates a variable using the syntax specific to the type of document
148
   * being edited.
149
   *
150
   * @param variable The variable to decorate in dot-notation without any
151
   *                 start or end sigils present.
152
   */
153
  private String decorate( final String variable ) {
154
    return getVariableDecorator().decorate( variable );
155
  }
156
157
  /**
158
   * Updates the text at the given position within the current paragraph.
159
   *
160
   * @param posBegan The starting index in the paragraph text to replace.
161
   * @param posEnded The ending index in the paragraph text to replace.
162
   * @param text     Overwrite the paragraph substring with this text.
163
   */
164
  private void replaceText(
165
      final int posBegan, final int posEnded, final String text ) {
166
    final int p = getCurrentParagraph();
167
168
    getEditor().replaceText( p, posBegan, p, posEnded, text );
169
  }
170
171
  /**
172
   * Returns the caret's current paragraph position.
173
   *
174
   * @return A number greater than or equal to 0.
175
   */
176
  private int getCurrentParagraph() {
177
    return getEditor().getCurrentParagraph();
178
  }
179
180
  /**
181
   * Returns the text for the paragraph that contains the caret.
182
   *
183
   * @return A non-null string, possibly empty.
184
   */
185
  private String getCaretParagraph() {
186
    return getEditor().getText( getCurrentParagraph() );
187
  }
188
189
  /**
190
   * Returns the caret position within the current paragraph.
191
   *
192
   * @return A value from 0 to the length of the current paragraph.
193
   */
194
  private int getCurrentCaretColumn() {
195
    return getEditor().getCaretColumn();
196
  }
197
198
  /**
199
   * Looks for the given word, matching first by exact, next by a starts-with
200
   * condition with diacritics replaced, then by containment.
201
   *
202
   * @param word
203
   * @return
204
   */
205
  @SuppressWarnings("ConstantConditions")
206
  private VariableTreeItem<String> findLeaf( final String word ) {
207
    assert word != null;
208
209
    final var pane = getDefinitionPane();
210
    VariableTreeItem<String> leaf = null;
211
212
    leaf = leaf == null ? pane.findLeafExact( word ) : leaf;
213
    leaf = leaf == null ? pane.findLeafStartsWith( word ) : leaf;
214
    leaf = leaf == null ? pane.findLeafContains( word ) : leaf;
215
    leaf = leaf == null ? pane.findLeafContainsNoCase( word ) : leaf;
216
217
    return leaf;
218
  }
219
220
  /**
221
   * Collapses the tree then expands and selects the given node.
222
   *
223
   * @param node The node to expand.
224
   */
225
  private void expand( final TreeItem<String> node ) {
226
    final DefinitionPane pane = getDefinitionPane();
227
    pane.collapse();
228
    pane.expand( node );
229
    pane.select( node );
230
  }
231
232
  /**
233
   * @return A variable decorator that corresponds to the given file type.
234
   */
235
  private VariableDecorator getVariableDecorator() {
236
    return VariableNameDecoratorFactory.newInstance( getFilename() );
237
  }
238
239
  private Path getFilename() {
240
    return getFileEditorTab().getPath();
241
  }
242
243
  private EditorPane getEditorPane() {
244
    return getFileEditorTab().getEditorPane();
245
  }
246
247
  private StyledTextArea<?, ?> getEditor() {
248
    return getEditorPane().getEditor();
249
  }
250
251
  public FileEditorTab getFileEditorTab() {
252
    return mTab;
253
  }
254
255
  private DefinitionPane getDefinitionPane() {
256
    return mDefinitionPane;
257
  }
258
}
1259
A src/main/java/com/scrivenvar/editors/markdown/HyperlinkModel.java
1
/*
2
 * Copyright 2020 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, url, and title.
34
 */
35
public class HyperlinkModel {
36
37
  private String text;
38
  private String url;
39
  private String title;
40
41
  /**
42
   * Constructs a new hyperlink model in Markdown format by default with no
43
   * title (i.e., tooltip).
44
   *
45
   * @param text The hyperlink text displayed (e.g., displayed to the user).
46
   * @param url  The destination URL (e.g., when clicked).
47
   */
48
  public HyperlinkModel( final String text, final String url ) {
49
    this( text, url, null );
50
  }
51
52
  /**
53
   * Constructs a new hyperlink model for the given AST link.
54
   *
55
   * @param link A markdown link.
56
   */
57
  public HyperlinkModel( final Link link ) {
58
    this(
59
        link.getText().toString(),
60
        link.getUrl().toString(),
61
        link.getTitle().toString()
62
    );
63
  }
64
65
  /**
66
   * Constructs a new hyperlink model in Markdown format by default.
67
   *
68
   * @param text  The hyperlink text displayed (e.g., displayed to the user).
69
   * @param url   The destination URL (e.g., when clicked).
70
   * @param title The hyperlink title (e.g., shown as a tooltip).
71
   */
72
  public HyperlinkModel( final String text, final String url,
73
                         final String title ) {
74
    setText( text );
75
    setUrl( url );
76
    setTitle( title );
77
  }
78
79
  /**
80
   * Returns the string in Markdown format by default.
81
   *
82
   * @return A markdown version of the hyperlink.
83
   */
84
  @Override
85
  public String toString() {
86
    String format = "%s%s%s";
87
88
    if( hasText() ) {
89
      format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
90
    }
91
92
    // Becomes ""+URL+"" if no text is set.
93
    // Becomes [TITLE]+(URL)+"" if no title is set.
94
    // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
95
    return String.format( format, getText(), getUrl(), getTitle() );
96
  }
97
98
  public final void setText( final String text ) {
99
    this.text = nullSafe( text );
100
  }
101
102
  public final void setUrl( final String url ) {
103
    this.url = nullSafe( url );
104
  }
105
106
  public final void setTitle( final String title ) {
107
    this.title = nullSafe( title );
108
  }
109
110
  /**
111
   * Answers whether text has been set for the hyperlink.
112
   *
113
   * @return true This is a text link.
114
   */
115
  public boolean hasText() {
116
    return !getText().isEmpty();
117
  }
118
119
  /**
120
   * Answers whether a title (tooltip) has been set for the hyperlink.
121
   *
122
   * @return true There is a title.
123
   */
124
  public boolean hasTitle() {
125
    return !getTitle().isEmpty();
126
  }
127
128
  public String getText() {
129
    return this.text;
130
  }
131
132
  public String getUrl() {
133
    return this.url;
134
  }
135
136
  public String getTitle() {
137
    return this.title;
138
  }
139
140
  private String nullSafe( final String s ) {
141
    return s == null ? "" : s;
142
  }
143
}
1144
A src/main/java/com/scrivenvar/editors/markdown/LinkVisitor.java
1
/*
2
 * Copyright 2020 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.util.ast.Node;
32
import com.vladsch.flexmark.util.ast.NodeVisitor;
33
import com.vladsch.flexmark.util.ast.VisitHandler;
34
35
/**
36
 * Responsible for extracting a hyperlink from the document so that the user
37
 * can edit the link within a dialog.
38
 */
39
public class LinkVisitor {
40
41
  private NodeVisitor visitor;
42
  private Link link;
43
  private final int offset;
44
45
  /**
46
   * Creates a hyperlink given an offset into a paragraph and the markdown AST
47
   * link node.
48
   *
49
   * @param index Index into the paragraph that indicates the hyperlink to
50
   *              change.
51
   */
52
  public LinkVisitor( final int index ) {
53
    this.offset = index;
54
  }
55
56
  public Link process( final Node root ) {
57
    getVisitor().visit( root );
58
    return getLink();
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 2020 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 com.scrivenvar.dialogs.ImageDialog;
31
import com.scrivenvar.dialogs.LinkDialog;
32
import com.scrivenvar.editors.EditorPane;
33
import com.scrivenvar.processors.markdown.BlockExtension;
34
import com.scrivenvar.processors.markdown.MarkdownProcessor;
35
import com.vladsch.flexmark.ast.Link;
36
import com.vladsch.flexmark.html.renderer.AttributablePart;
37
import com.vladsch.flexmark.util.ast.Node;
38
import com.vladsch.flexmark.util.html.MutableAttributes;
39
import javafx.scene.control.Dialog;
40
import javafx.scene.control.IndexRange;
41
import javafx.scene.input.KeyCode;
42
import javafx.scene.input.KeyEvent;
43
import javafx.stage.Window;
44
import org.fxmisc.richtext.StyleClassedTextArea;
45
46
import java.nio.file.Path;
47
import java.util.ArrayList;
48
import java.util.List;
49
import java.util.regex.Matcher;
50
import java.util.regex.Pattern;
51
52
import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN;
53
import static com.scrivenvar.util.Utils.ltrim;
54
import static com.scrivenvar.util.Utils.rtrim;
55
import static javafx.scene.input.KeyCode.ENTER;
56
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
57
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
58
59
/**
60
 * Provides the ability to edit a text document.
61
 */
62
public class MarkdownEditorPane extends EditorPane {
63
  private static final Pattern PATTERN_AUTO_INDENT = Pattern.compile(
64
      "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
65
66
  /**
67
   * Any of these followed by a space and a letter produce a line
68
   * by themselves. The ">" need not be followed by a space.
69
   */
70
  private static final Pattern PATTERN_NEW_LINE = Pattern.compile(
71
      "^>|(((#+)|([*+\\-])|([1-9]\\.))\\s+).+" );
72
73
  public MarkdownEditorPane() {
74
    initEditor();
75
  }
76
77
  private void initEditor() {
78
    final StyleClassedTextArea textArea = getEditor();
79
80
    textArea.setWrapText( true );
81
    textArea.getStyleClass().add( "markdown-editor" );
82
    textArea.getStylesheets().add( STYLESHEET_MARKDOWN );
83
84
    addKeyboardListener( keyPressed( ENTER ), this::enterPressed );
85
    addKeyboardListener( keyPressed( KeyCode.X, CONTROL_DOWN ), this::cut );
86
  }
87
88
  public void insertLink() {
89
    insertObject( createLinkDialog() );
90
  }
91
92
  public void insertImage() {
93
    insertObject( createImageDialog() );
94
  }
95
96
  /**
97
   * Returns the editor's paragraph number that will be close to its HTML
98
   * paragraph ID. Ultimately this solution is flawed because there isn't
99
   * a straightforward correlation between the document being edited and
100
   * what is rendered. XML documents transformed through stylesheets have
101
   * no readily determined correlation. Images, tables, and other
102
   * objects affect the relative location of the current paragraph being
103
   * edited with respect to the preview pane.
104
   * <p>
105
   * See
106
   * {@link BlockExtension.IdAttributeProvider#setAttributes(Node, AttributablePart, MutableAttributes)}}
107
   * for details.
108
   * </p>
109
   * <p>
110
   * Injecting a token into the document, as per a previous version of the
111
   * application, can instruct the preview pane where to shift the viewport.
112
   * </p>
113
   *
114
   * @param paraIndex The paragraph index from the editor pane to scroll to
115
   *                  in the preview pane, which  will be approximated if an
116
   *                  equivalent cannot be found.
117
   * @return A unique identifier that correlates to an equivalent paragraph
118
   * number once the Markdown is rendered into HTML.
119
   */
120
  public int approximateParagraphId( final int paraIndex ) {
121
    final StyleClassedTextArea editor = getEditor();
122
    final List<String> lines = new ArrayList<>( 4096 );
123
124
    int i = 0;
125
    String prevText = "";
126
    boolean withinFencedBlock = false;
127
    boolean withinCodeBlock = false;
128
129
    for( final var p : editor.getParagraphs() ) {
130
      if( i > paraIndex ) {
131
        break;
132
      }
133
134
      final String text = p.getText().replace( '>', ' ' );
135
      if( text.startsWith( "```" ) ) {
136
        if( withinFencedBlock = !withinFencedBlock ) {
137
          lines.add( text );
138
        }
139
      }
140
141
      if( !withinFencedBlock ) {
142
        final boolean foundCodeBlock = text.startsWith( "    " );
143
144
        if( foundCodeBlock && !withinCodeBlock ) {
145
          lines.add( text );
146
          withinCodeBlock = true;
147
        }
148
        else if( !foundCodeBlock ) {
149
          withinCodeBlock = false;
150
        }
151
      }
152
153
      if( !withinFencedBlock && !withinCodeBlock &&
154
          ((!text.isBlank() && prevText.isBlank()) ||
155
              PATTERN_NEW_LINE.matcher( text ).matches()) ) {
156
        lines.add( text );
157
      }
158
159
      prevText = text;
160
      i++;
161
    }
162
163
    // Scrolling index is 1-based.
164
    return Math.max( lines.size() - 1, 0 );
165
  }
166
167
  /**
168
   * Gets the index of the paragraph where the caret is positioned.
169
   *
170
   * @return The paragraph number for the caret.
171
   */
172
  public int getCurrentParagraphIndex() {
173
    return getEditor().getCurrentParagraph();
174
  }
175
176
  /**
177
   * @param leading  Characters to insert at the beginning of the current
178
   *                 selection (or paragraph).
179
   * @param trailing Characters to insert at the end of the current selection
180
   *                 (or paragraph).
181
   */
182
  public void surroundSelection( final String leading, final String trailing ) {
183
    surroundSelection( leading, trailing, null );
184
  }
185
186
  /**
187
   * @param leading  Characters to insert at the beginning of the current
188
   *                 selection (or paragraph).
189
   * @param trailing Characters to insert at the end of the current selection
190
   *                 (or paragraph).
191
   * @param hint     Instructional text inserted within the leading and
192
   *                 trailing characters, provided no text is selected.
193
   */
194
  public void surroundSelection(
195
      String leading, String trailing, final String hint ) {
196
    final StyleClassedTextArea textArea = getEditor();
197
198
    // Note: not using textArea.insertText() to insert leading and trailing
199
    // because this would add two changes to undo history
200
    final IndexRange selection = textArea.getSelection();
201
    int start = selection.getStart();
202
    int end = selection.getEnd();
203
204
    final String selectedText = textArea.getSelectedText();
205
206
    String trimmedText = selectedText.trim();
207
    if( trimmedText.length() < selectedText.length() ) {
208
      start += selectedText.indexOf( trimmedText );
209
      end = start + trimmedText.length();
210
    }
211
212
    // remove leading whitespaces from leading text if selection starts at zero
213
    if( start == 0 ) {
214
      leading = ltrim( leading );
215
    }
216
217
    // remove trailing whitespaces from trailing text if selection ends at
218
    // text end
219
    if( end == textArea.getLength() ) {
220
      trailing = rtrim( trailing );
221
    }
222
223
    // remove leading line separators from leading text
224
    // if there are line separators before the selected text
225
    if( leading.startsWith( "\n" ) ) {
226
      for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
227
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
228
          break;
229
        }
230
231
        leading = leading.substring( 1 );
232
      }
233
    }
234
235
    // remove trailing line separators from trailing or leading text
236
    // if there are line separators after the selected text
237
    final boolean trailingIsEmpty = trailing.isEmpty();
238
    String str = trailingIsEmpty ? leading : trailing;
239
240
    if( str.endsWith( "\n" ) ) {
241
      final int length = textArea.getLength();
242
243
      for( int i = end; i < length && str.endsWith( "\n" ); i++ ) {
244
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
245
          break;
246
        }
247
248
        str = str.substring( 0, str.length() - 1 );
249
      }
250
251
      if( trailingIsEmpty ) {
252
        leading = str;
253
      }
254
      else {
255
        trailing = str;
256
      }
257
    }
258
259
    int selStart = start + leading.length();
260
    int selEnd = end + leading.length();
261
262
    // insert hint text if selection is empty
263
    if( hint != null && trimmedText.isEmpty() ) {
264
      trimmedText = hint;
265
      selEnd = selStart + hint.length();
266
    }
267
268
    // prevent undo merging with previous text entered by user
269
    getUndoManager().preventMerge();
270
271
    // replace text and update selection
272
    textArea.replaceText( start, end, leading + trimmedText + trailing );
273
    textArea.selectRange( selStart, selEnd );
274
  }
275
276
  private void enterPressed( final KeyEvent e ) {
277
    final StyleClassedTextArea textArea = getEditor();
278
    final String currentLine =
279
        textArea.getText( textArea.getCurrentParagraph() );
280
    final Matcher matcher = PATTERN_AUTO_INDENT.matcher( currentLine );
281
282
    String newText = "\n";
283
284
    if( matcher.matches() ) {
285
      if( !matcher.group( 2 ).isEmpty() ) {
286
        // indent new line with same whitespace characters and list markers
287
        // as current line
288
        newText = newText.concat( matcher.group( 1 ) );
289
      }
290
      else {
291
        // current line contains only whitespace characters and list markers
292
        // --> empty current line
293
        final int caretPosition = textArea.getCaretPosition();
294
        textArea.selectRange( caretPosition - currentLine.length(),
295
                              caretPosition );
296
      }
297
    }
298
299
    textArea.replaceSelection( newText );
300
301
    // Ensure that the window scrolls when Enter is pressed at the bottom of
302
    // the pane.
303
    textArea.requestFollowCaret();
304
  }
305
306
  private void cut( final KeyEvent event ) {
307
    super.cut();
308
  }
309
310
  /**
311
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
312
   * the markdown AST.
313
   *
314
   * @return An instance containing the link URL and display text.
315
   */
316
  private HyperlinkModel getHyperlink() {
317
    final StyleClassedTextArea textArea = getEditor();
318
    final String selectedText = textArea.getSelectedText();
319
320
    // Get the current paragraph, convert to Markdown nodes.
321
    final MarkdownProcessor mp = new MarkdownProcessor( null );
322
    final int p = textArea.getCurrentParagraph();
323
    final String paragraph = textArea.getText( p );
324
    final Node node = mp.toNode( paragraph );
325
    final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() );
326
    final Link link = visitor.process( node );
327
328
    if( link != null ) {
329
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
330
    }
331
332
    return createHyperlinkModel(
333
        link, selectedText, "https://localhost"
334
    );
335
  }
336
337
  @SuppressWarnings("SameParameterValue")
338
  private HyperlinkModel createHyperlinkModel(
339
      final Link link, final String selection, final String url ) {
340
341
    return link == null
342
        ? new HyperlinkModel( selection, url )
343
        : new HyperlinkModel( link );
344
  }
345
346
  private Path getParentPath() {
347
    final Path path = getPath();
348
    return (path != null) ? path.getParent() : null;
349
  }
350
351
  private Dialog<String> createLinkDialog() {
352
    return new LinkDialog( getWindow(), getHyperlink() );
353
  }
354
355
  private Dialog<String> createImageDialog() {
356
    return new ImageDialog( getWindow(), getParentPath() );
357
  }
358
359
  private void insertObject( final Dialog<String> dialog ) {
360
    dialog.showAndWait().ifPresent(
361
        result -> getEditor().replaceSelection( result )
362
    );
363
  }
364
365
  private Window getWindow() {
366
    return getScrollPane().getScene().getWindow();
367
  }
368
}
1369
A src/main/java/com/scrivenvar/predicates/files/FileTypePredicate.java
1
/*
2
 * Copyright 2020 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
public class FileTypePredicate implements Predicate<File> {
41
42
  private final PathMatcher mMatcher;
43
44
  /**
45
   * Constructs a new instance given a set of file extension globs.
46
   *
47
   * @param patterns Comma-separated list of globbed extensions including the
48
   * Kleene star (e.g., <code>*.md,*.markdown,*.txt</code>).
49
   */
50
  public FileTypePredicate( final String patterns ) {
51
    mMatcher = FileSystems.getDefault().getPathMatcher(
52
      "glob:**{" + patterns + "}"
53
    );
54
  }
55
56
  /**
57
   * Constructs a new instance given a list of file extension globs, each must
58
   * include the Kleene star (a.k.a. asterisk).
59
   *
60
   * @param patterns Collection of globbed extensions.
61
   */
62
  public FileTypePredicate( final Collection<String> patterns ) {
63
    this( String.join( ",", patterns ) );
64
  }
65
66
  /**
67
   * Returns true if the file matches the patterns defined during construction.
68
   *
69
   * @param file The filename to match against the given glob patterns.
70
   *
71
   * @return false The filename does not match the glob patterns.
72
   */
73
  @Override
74
  public boolean test( final File file ) {
75
    return getMatcher().matches( file.toPath() );
76
  }
77
78
  private PathMatcher getMatcher() {
79
    return mMatcher;
80
  }
81
}
182
A src/main/java/com/scrivenvar/predicates/strings/ContainsPredicate.java
1
/*
2
 * Copyright 2020 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
public class ContainsPredicate extends StringPredicate {
34
35
  /**
36
   * Calls the superclass to construct the instance.
37
   *
38
   * @param comparate Not null.
39
   */
40
  public ContainsPredicate( final String comparate ) {
41
    super( comparate );
42
  }
43
44
  /**
45
   * Answers whether the given strings match each other. What match means will
46
   * depend on user preferences. The empty condition is required to return the
47
   * first node in a list of child nodes when the user has not yet selected a
48
   * node.
49
   *
50
   * @param comparator The string to compare against the comparate.
51
   *
52
   * @return true if s1 and s2 are a match according to some criteria,or s2 is
53
   * empty.
54
   */
55
  @Override
56
  public boolean test( final String comparator ) {
57
    final String comparate = getComparate().toLowerCase();
58
    return comparator.contains( comparate.toLowerCase() )
59
      || comparate.isEmpty();
60
  }
61
}
162
A src/main/java/com/scrivenvar/predicates/strings/StartsPredicate.java
1
/*
2
 * Copyright 2020 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
public class StartsPredicate extends StringPredicate {
34
35
  /**
36
   * Constructs a new instance using a comparate that will be compared with
37
   * the comparator during the test.
38
   *
39
   * @param comparate The string to compare against the comparator.
40
   */
41
  public StartsPredicate( final String comparate ) {
42
    super( comparate );
43
  }
44
45
  /**
46
   * Compares two strings.
47
   *
48
   * @param comparator A non-null string, possibly empty.
49
   *
50
   * @return true The comparator starts with the comparate, ignoring case.
51
   */
52
  @Override
53
  public boolean test( final String comparator ) {
54
    final String comparate = getComparate().toLowerCase();
55
    return comparator.startsWith( comparate.toLowerCase() );
56
  }
57
}
158
A src/main/java/com/scrivenvar/predicates/strings/StringPredicate.java
1
/*
2
 * Copyright 2020 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
public abstract class StringPredicate implements Predicate<String> {
36
37
  private final String comparate;
38
39
  public StringPredicate( final String comparate ) {
40
    this.comparate = comparate;
41
  }
42
43
  protected String getComparate() {
44
    return this.comparate;
45
  }
46
}
147
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 com.scrivenvar.Services;
31
import com.scrivenvar.service.events.Notifier;
32
33
import java.io.File;
34
import java.io.FileInputStream;
35
import java.io.FileOutputStream;
36
import java.util.*;
37
import java.util.prefs.AbstractPreferences;
38
import java.util.prefs.BackingStoreException;
39
40
/**
41
 * Preferences implementation that stores to a user-defined file. Local file
42
 * storage is preferred over a certain operating system's monolithic trash heap
43
 * called a registry. When the OS is locked down, the default Preferences
44
 * implementation will try to write to the registry and fail due to permissions
45
 * problems. This class sidesteps the issue entirely by writing to the user's
46
 * home directory, where permissions should be a bit more lax.
47
 */
48
public class FilePreferences extends AbstractPreferences {
49
  private final Notifier mNotifier = Services.load( Notifier.class );
50
51
  private final Map<String, String> mRoot = new TreeMap<>();
52
  private final Map<String, FilePreferences> mChildren = new TreeMap<>();
53
  private boolean mRemoved;
54
55
  private final Object mMutex = new Object();
56
57
  public FilePreferences(
58
      final AbstractPreferences parent, final String name ) {
59
    super( parent, name );
60
61
    try {
62
      sync();
63
    } catch( final BackingStoreException ex ) {
64
      error( ex );
65
    }
66
  }
67
68
  @Override
69
  protected void putSpi( final String key, final String value ) {
70
    synchronized( mMutex ) {
71
      mRoot.put( key, value );
72
    }
73
74
    try {
75
      flush();
76
    } catch( final BackingStoreException ex ) {
77
      error( ex );
78
    }
79
  }
80
81
  @Override
82
  protected String getSpi( final String key ) {
83
    synchronized( mMutex ) {
84
      return mRoot.get( key );
85
    }
86
  }
87
88
  @Override
89
  protected void removeSpi( final String key ) {
90
    synchronized( mMutex ) {
91
      mRoot.remove( key );
92
    }
93
94
    try {
95
      flush();
96
    } catch( final BackingStoreException ex ) {
97
      error( ex );
98
    }
99
  }
100
101
  @Override
102
  protected void removeNodeSpi() throws BackingStoreException {
103
    mRemoved = true;
104
    flush();
105
  }
106
107
  @Override
108
  protected String[] keysSpi() {
109
    synchronized( mMutex ) {
110
      return mRoot.keySet().toArray( new String[ 0 ] );
111
    }
112
  }
113
114
  @Override
115
  protected String[] childrenNamesSpi() {
116
    return mChildren.keySet().toArray( new String[ 0 ] );
117
  }
118
119
  @Override
120
  protected FilePreferences childSpi( final String name ) {
121
    FilePreferences child = mChildren.get( name );
122
123
    if( child == null || child.isRemoved() ) {
124
      child = new FilePreferences( this, name );
125
      mChildren.put( name, child );
126
    }
127
128
    return child;
129
  }
130
131
  @Override
132
  protected void syncSpi() {
133
    if( isRemoved() ) {
134
      return;
135
    }
136
137
    final File file = FilePreferencesFactory.getPreferencesFile();
138
139
    if( !file.exists() ) {
140
      return;
141
    }
142
143
    synchronized( mMutex ) {
144
      final Properties p = new Properties();
145
146
      try( final var inputStream = new FileInputStream( file ) ) {
147
        p.load( inputStream );
148
149
        final String path = getPath();
150
        final Enumeration<?> propertyNames = p.propertyNames();
151
152
        while( propertyNames.hasMoreElements() ) {
153
          final String propKey = (String) propertyNames.nextElement();
154
155
          if( propKey.startsWith( path ) ) {
156
            final String subKey = propKey.substring( path.length() );
157
158
            // Only load immediate descendants
159
            if( subKey.indexOf( '.' ) == -1 ) {
160
              mRoot.put( subKey, p.getProperty( propKey ) );
161
            }
162
          }
163
        }
164
      } catch( final Exception ex ) {
165
        error( new BackingStoreException( ex ) );
166
      }
167
    }
168
  }
169
170
  private String getPath() {
171
    final FilePreferences parent = (FilePreferences) parent();
172
173
    return parent == null ? "" : parent.getPath() + name() + '.';
174
  }
175
176
  @Override
177
  protected void flushSpi() {
178
    final File file = FilePreferencesFactory.getPreferencesFile();
179
180
    synchronized( mMutex ) {
181
      final Properties p = new Properties();
182
183
      try {
184
        final String path = getPath();
185
186
        if( file.exists() ) {
187
          try( final var fis = new FileInputStream( file ) ) {
188
            p.load( fis );
189
          }
190
191
          final List<String> toRemove = new ArrayList<>();
192
193
          // Make a list of all direct children of this node to be removed
194
          final Enumeration<?> propertyNames = p.propertyNames();
195
196
          while( propertyNames.hasMoreElements() ) {
197
            final String propKey = (String) propertyNames.nextElement();
198
            if( propKey.startsWith( path ) ) {
199
              final String subKey = propKey.substring( path.length() );
200
201
              // Only do immediate descendants
202
              if( subKey.indexOf( '.' ) == -1 ) {
203
                toRemove.add( propKey );
204
              }
205
            }
206
          }
207
208
          // Remove them now that the enumeration is done with
209
          for( final String propKey : toRemove ) {
210
            p.remove( propKey );
211
          }
212
        }
213
214
        // If this node hasn't been removed, add back in any values
215
        if( !mRemoved ) {
216
          for( final String s : mRoot.keySet() ) {
217
            p.setProperty( path + s, mRoot.get( s ) );
218
          }
219
        }
220
221
        try( final var fos = new FileOutputStream( file ) ) {
222
          p.store( fos, "FilePreferences" );
223
        }
224
      } catch( final Exception ex ) {
225
        error( new BackingStoreException( ex ) );
226
      }
227
    }
228
  }
229
230
  private void error( final BackingStoreException ex ) {
231
    getNotifier().notify( ex );
232
  }
233
234
  private Notifier getNotifier() {
235
    return mNotifier;
236
  }
237
}
1238
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 java.io.File;
31
import java.nio.file.FileSystems;
32
import java.util.prefs.Preferences;
33
import java.util.prefs.PreferencesFactory;
34
35
import static com.scrivenvar.Constants.APP_TITLE;
36
37
/**
38
 * PreferencesFactory implementation that stores the preferences in a
39
 * user-defined file. Usage:
40
 * <pre>
41
 * System.setProperty( "java.util.prefs.PreferencesFactory",
42
 * FilePreferencesFactory.class.getName() );
43
 * </pre>
44
 * <p>
45
 * The file defaults to <code>$user.home/.scrivenvar</code>, but can be changed
46
 * using <code>-Dapplication.name=preferences</code> when running the
47
 * application, or by calling <code>System.setProperty</code> with the
48
 * "application.name" property.
49
 * </p>
50
 */
51
public class FilePreferencesFactory implements PreferencesFactory {
52
53
  private static File preferencesFile;
54
  private Preferences rootPreferences;
55
56
  @Override
57
  public Preferences systemRoot() {
58
    return userRoot();
59
  }
60
61
  @Override
62
  public synchronized Preferences userRoot() {
63
    if( rootPreferences == null ) {
64
      rootPreferences = new FilePreferences( null, "" );
65
    }
66
67
    return rootPreferences;
68
  }
69
70
  public synchronized static File getPreferencesFile() {
71
    if( preferencesFile == null ) {
72
      String prefsFile = getPreferencesFilename();
73
74
      preferencesFile = new File( prefsFile ).getAbsoluteFile();
75
    }
76
77
    return preferencesFile;
78
  }
79
80
  public static String getPreferencesFilename() {
81
    final String filename = System.getProperty( "application.name", APP_TITLE );
82
    return System.getProperty( "user.home" ) + getSeparator() + "." + filename;
83
  }
84
85
  public static String getSeparator() {
86
    return FileSystems.getDefault().getSeparator();
87
  }
88
}
189
A src/main/java/com/scrivenvar/preferences/UserPreferences.java
1
/*
2
 * Copyright 2020 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 com.dlsc.formsfx.model.structure.StringField;
31
import com.dlsc.preferencesfx.PreferencesFx;
32
import com.dlsc.preferencesfx.model.Category;
33
import com.dlsc.preferencesfx.model.Group;
34
import com.dlsc.preferencesfx.model.Setting;
35
import com.scrivenvar.Services;
36
import com.scrivenvar.service.Settings;
37
import javafx.beans.property.ObjectProperty;
38
import javafx.beans.property.SimpleObjectProperty;
39
import javafx.beans.property.SimpleStringProperty;
40
import javafx.beans.property.StringProperty;
41
import javafx.scene.Node;
42
import javafx.scene.control.Label;
43
44
import java.io.File;
45
import java.nio.file.Path;
46
47
import static com.scrivenvar.Constants.*;
48
import static com.scrivenvar.Messages.get;
49
50
/**
51
 * Responsible for user preferences that can be changed from the GUI. The
52
 * settings are displayed and persisted using {@link PreferencesFx}.
53
 */
54
public class UserPreferences {
55
  private final Settings SETTINGS = Services.load( Settings.class );
56
57
  private final ObjectProperty<File> mPropRDirectory;
58
  private final StringProperty mPropRScript;
59
  private final ObjectProperty<File> mPropImagesDirectory;
60
  private final StringProperty mPropImagesOrder;
61
  private final ObjectProperty<File> mPropDefinitionPath;
62
  private final StringProperty mRDelimiterBegan;
63
  private final StringProperty mRDelimiterEnded;
64
65
  private final PreferencesFx mPreferencesFx;
66
67
  public UserPreferences() {
68
    mPropRDirectory = simpleFile( USER_DIRECTORY );
69
    mPropRScript = new SimpleStringProperty( "" );
70
71
    mPropImagesDirectory = simpleFile( USER_DIRECTORY );
72
    mPropImagesOrder = new SimpleStringProperty( PERSIST_IMAGES_DEFAULT );
73
74
    mPropDefinitionPath = simpleFile( getSetting(
75
        "file.definition.default", "variables.yaml" )
76
    );
77
78
    mRDelimiterBegan = new SimpleStringProperty( R_DELIMITER_BEGAN_DEFAULT );
79
    mRDelimiterEnded = new SimpleStringProperty( R_DELIMITER_ENDED_DEFAULT );
80
81
    mPreferencesFx = createPreferencesFx();
82
  }
83
84
  /**
85
   * Display the user preferences settings dialog (non-modal).
86
   */
87
  public void show() {
88
    mPreferencesFx.show( false );
89
  }
90
91
  /**
92
   * Call to persist the settings. Strictly speaking, this could watch on
93
   * all values for external changes then save automatically.
94
   */
95
  public void save() {
96
    mPreferencesFx.saveSettings();
97
  }
98
99
  /**
100
   * Creates the preferences dialog.
101
   * <p>
102
   * TODO: Make this dynamic by iterating over all "Preferences.*" values
103
   * that follow a particular naming pattern.
104
   * </p>
105
   *
106
   * @return A new instance of preferences for users to edit.
107
   */
108
  @SuppressWarnings("unchecked")
109
  private PreferencesFx createPreferencesFx() {
110
    final Setting<StringField, StringProperty> scriptSetting =
111
        Setting.of( "Script", mPropRScript );
112
    final StringField field = scriptSetting.getElement();
113
    field.multiline( true );
114
115
    return PreferencesFx.of(
116
        UserPreferences.class,
117
        Category.of(
118
            get( "Preferences.r" ),
119
            Group.of(
120
                get( "Preferences.r.directory" ),
121
                Setting.of( label( "Preferences.r.directory.desc", false ) ),
122
                Setting.of( "Directory", mPropRDirectory, true )
123
            ),
124
            Group.of(
125
                get( "Preferences.r.script" ),
126
                Setting.of( label( "Preferences.r.script.desc" ) ),
127
                scriptSetting
128
            ),
129
            Group.of(
130
                get( "Preferences.r.delimiter.began" ),
131
                Setting.of( label( "Preferences.r.delimiter.began.desc" ) ),
132
                Setting.of( "Opening", mRDelimiterBegan )
133
            ),
134
            Group.of(
135
                get( "Preferences.r.delimiter.ended" ),
136
                Setting.of( label( "Preferences.r.delimiter.ended.desc" ) ),
137
                Setting.of( "Closing", mRDelimiterEnded )
138
            )
139
        ),
140
        Category.of(
141
            get( "Preferences.images" ),
142
            Group.of(
143
                get( "Preferences.images.directory" ),
144
                Setting.of( label( "Preferences.images.directory.desc" ) ),
145
                Setting.of( "Directory", mPropImagesDirectory, true )
146
            ),
147
            Group.of(
148
                get( "Preferences.images.suffixes" ),
149
                Setting.of( label( "Preferences.images.suffixes.desc" ) ),
150
                Setting.of( "Extensions", mPropImagesOrder )
151
            )
152
        ),
153
        Category.of(
154
            get( "Preferences.definitions" ),
155
            Group.of(
156
                get( "Preferences.definitions.path" ),
157
                Setting.of( label( "Preferences.definitions.path.desc" ) ),
158
                Setting.of( "Path", mPropDefinitionPath, false )
159
            )
160
        )
161
    );
162
  }
163
164
  /**
165
   * Wraps a {@link File} inside a {@link SimpleObjectProperty}.
166
   *
167
   * @param path The file name to use when constructing the {@link File}.
168
   * @return A new {@link SimpleObjectProperty} instance with a {@link File}
169
   * that references the given {@code path}.
170
   */
171
  private SimpleObjectProperty<File> simpleFile( final String path ) {
172
    return new SimpleObjectProperty<>( new File( path ) );
173
  }
174
175
  /**
176
   * Creates a label for the given key after interpolating its value.
177
   *
178
   * @param key The key to find in the resource bundle.
179
   * @return The value of the key as a label.
180
   */
181
  private Node label( final String key ) {
182
    return new Label( get( key, true ) );
183
  }
184
185
  /**
186
   * Creates a label for the given key.
187
   *
188
   * @param key         The key to find in the resource bundle.
189
   * @param interpolate {@code true} means to interpolate the value.
190
   * @return The value of the key, interpolated if {@code interpolate} is
191
   * {@code true}.
192
   */
193
  @SuppressWarnings("SameParameterValue")
194
  private Node label( final String key, final boolean interpolate ) {
195
    return new Label( get( key, interpolate ) );
196
  }
197
198
  /**
199
   * Returns the value for a key from the settings properties file.
200
   *
201
   * @param key   Key within the settings properties file to find.
202
   * @param value Default value to return if the key is not found.
203
   * @return The value for the given key from the settings file, or the
204
   * given {@code value} if no key found.
205
   */
206
  @SuppressWarnings("SameParameterValue")
207
  private String getSetting( final String key, final String value ) {
208
    return SETTINGS.getSetting( key, value );
209
  }
210
211
  public ObjectProperty<File> definitionPathProperty() {
212
    return mPropDefinitionPath;
213
  }
214
215
  public Path getDefinitionPath() {
216
    return definitionPathProperty().getValue().toPath();
217
  }
218
219
  private ObjectProperty<File> rDirectoryProperty() {
220
    return mPropRDirectory;
221
  }
222
223
  public File getRDirectory() {
224
    return rDirectoryProperty().getValue();
225
  }
226
227
  private StringProperty rScriptProperty() {
228
    return mPropRScript;
229
  }
230
231
  public String getRScript() {
232
    return rScriptProperty().getValue();
233
  }
234
235
  private StringProperty rDelimiterBegan() {
236
    return mRDelimiterBegan;
237
  }
238
239
  public String getRDelimiterBegan() {
240
    return rDelimiterBegan().get();
241
  }
242
243
  private StringProperty rDelimiterEnded() {
244
    return mRDelimiterEnded;
245
  }
246
247
  public String getRDelimiterEnded() {
248
    return rDelimiterEnded().get();
249
  }
250
251
  private ObjectProperty<File> imagesDirectoryProperty() {
252
    return mPropImagesDirectory;
253
  }
254
255
  public File getImagesDirectory() {
256
    return imagesDirectoryProperty().getValue();
257
  }
258
259
  private StringProperty imagesOrderProperty() {
260
    return mPropImagesOrder;
261
  }
262
263
  public String getImagesOrder() {
264
    return imagesOrderProperty().getValue();
265
  }
266
}
1267
A src/main/java/com/scrivenvar/preview/ChainedReplacedElementFactory.java
1
/*
2
 * {{{ header & license
3
 * Copyright 2006 Patrick Wright
4
 * Copyright 2007 Wisconsin Court System
5
 *
6
 * This program is free software; you can redistribute it and/or
7
 * modify it under the terms of the GNU Lesser General Public License
8
 * as published by the Free Software Foundation; either version 2.1
9
 * of the License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
14
 * GNU Lesser General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Lesser General Public License
17
 * along with this program; if not, write to the Free Software
18
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19
 * }}}
20
 */
21
package com.scrivenvar.preview;
22
23
import org.w3c.dom.Element;
24
import org.xhtmlrenderer.extend.ReplacedElement;
25
import org.xhtmlrenderer.extend.ReplacedElementFactory;
26
import org.xhtmlrenderer.extend.UserAgentCallback;
27
import org.xhtmlrenderer.layout.LayoutContext;
28
import org.xhtmlrenderer.render.BlockBox;
29
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
30
31
import java.util.ArrayList;
32
import java.util.List;
33
34
public class ChainedReplacedElementFactory implements ReplacedElementFactory {
35
  private final List<ReplacedElementFactory> mFactoryList = new ArrayList<>();
36
37
  public ChainedReplacedElementFactory() {
38
  }
39
40
  public ReplacedElement createReplacedElement(
41
      final LayoutContext c,
42
      final BlockBox box,
43
      final UserAgentCallback uac,
44
      final int cssWidth,
45
      final int cssHeight ) {
46
    for( final var f : mFactoryList ) {
47
      final var r = f.createReplacedElement( c, box, uac, cssWidth, cssHeight );
48
49
      if( r != null ) {
50
        return r;
51
      }
52
    }
53
54
    return null;
55
  }
56
57
  public void addFactory( final ReplacedElementFactory factory ) {
58
    mFactoryList.add( factory );
59
  }
60
61
  public void reset() {
62
    for( final var factory : mFactoryList ) {
63
      factory.reset();
64
    }
65
  }
66
67
  public void remove( final Element element ) {
68
    for( final var factory : mFactoryList ) {
69
      factory.remove( element );
70
    }
71
  }
72
73
  public void setFormSubmissionListener( FormSubmissionListener listener ) {
74
  }
75
}
176
A src/main/java/com/scrivenvar/preview/CustomImageLoader.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.util.ProtocolResolver;
31
import javafx.beans.property.IntegerProperty;
32
import javafx.beans.property.SimpleIntegerProperty;
33
import org.xhtmlrenderer.extend.FSImage;
34
import org.xhtmlrenderer.resource.ImageResource;
35
import org.xhtmlrenderer.swing.ImageResourceLoader;
36
37
import java.net.URI;
38
import java.nio.file.Files;
39
import java.nio.file.Paths;
40
41
import static com.scrivenvar.preview.SVGRasterizer.BROKEN_IMAGE_PLACEHOLDER;
42
import static org.xhtmlrenderer.swing.AWTFSImage.createImage;
43
44
/**
45
 * Responsible for loading images. If the image cannot be found, a placeholder
46
 * is used instead.
47
 */
48
public class CustomImageLoader extends ImageResourceLoader {
49
  /**
50
   * Placeholder that's displayed when image cannot be found.
51
   */
52
  private static final FSImage BROKEN_IMAGE = createImage(
53
      BROKEN_IMAGE_PLACEHOLDER );
54
55
  private final IntegerProperty mWidthProperty = new SimpleIntegerProperty();
56
57
  public CustomImageLoader() {
58
  }
59
60
  /**
61
   * Gets an {@link IntegerProperty} that represents the maximum width an
62
   * image should be scaled.
63
   *
64
   * @return The maximum width for an image.
65
   */
66
  public IntegerProperty widthProperty() {
67
    return mWidthProperty;
68
  }
69
70
  /**
71
   * Gets an image resolved from the given URI. If the image cannot be found,
72
   * this will return a custom placeholder image indicating the reference
73
   * is broken.
74
   *
75
   * @param uri    Path to the image resource to load.
76
   * @param width  Ignored.
77
   * @param height Ignored.
78
   * @return The scaled image, or a placeholder image if the URI's content
79
   * could not be retrieved.
80
   */
81
  @Override
82
  public synchronized ImageResource get(
83
      final String uri, final int width, final int height ) {
84
    assert uri != null;
85
    assert width >= 0;
86
    assert height >= 0;
87
88
    boolean exists = true;
89
90
    try {
91
      final String protocol = ProtocolResolver.getProtocol( uri );
92
93
      if( "file".equals( protocol ) ) {
94
        exists = Files.exists( Paths.get( new URI( uri ) ) );
95
      }
96
    } catch( final Exception e ) {
97
      exists = false;
98
    }
99
100
    return exists
101
        ? scale( uri, width, height )
102
        : new ImageResource( uri, BROKEN_IMAGE );
103
  }
104
105
  /**
106
   * Scales the image found at the given URI.
107
   *
108
   * @param uri Path to the image file to load.
109
   * @param w   Ignored.
110
   * @param h   Ignored.
111
   * @return Resource representing the rendered image and path.
112
   */
113
  private ImageResource scale( final String uri, final int w, final int h ) {
114
    final var ir = super.get( uri, w, h );
115
    final var image = ir.getImage();
116
    final var imageWidth = image.getWidth();
117
    final var imageHeight = image.getHeight();
118
119
    int maxWidth = mWidthProperty.get();
120
    int newWidth = imageWidth;
121
    int newHeight = imageHeight;
122
123
    // Maintain aspect ratio while shrinking image to view port bounds.
124
    if( imageWidth > maxWidth ) {
125
      newWidth = maxWidth;
126
      newHeight = (newWidth * imageHeight) / imageWidth;
127
    }
128
129
    image.scale( newWidth, newHeight );
130
    return ir;
131
  }
132
}
1133
A src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.Services;
31
import com.scrivenvar.service.events.Notifier;
32
import javafx.beans.property.BooleanProperty;
33
import javafx.beans.property.SimpleBooleanProperty;
34
import javafx.beans.value.ChangeListener;
35
import javafx.beans.value.ObservableValue;
36
import javafx.embed.swing.SwingNode;
37
import javafx.scene.Node;
38
import javafx.scene.layout.Pane;
39
import org.jsoup.Jsoup;
40
import org.jsoup.helper.W3CDom;
41
import org.jsoup.nodes.Document;
42
import org.xhtmlrenderer.event.DocumentListener;
43
import org.xhtmlrenderer.layout.SharedContext;
44
import org.xhtmlrenderer.render.Box;
45
import org.xhtmlrenderer.simple.XHTMLPanel;
46
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
47
import org.xhtmlrenderer.swing.*;
48
49
import javax.swing.*;
50
import java.awt.*;
51
import java.awt.event.ComponentEvent;
52
import java.awt.event.ComponentListener;
53
import java.net.URI;
54
import java.nio.file.Path;
55
56
import static com.scrivenvar.Constants.*;
57
import static java.awt.Desktop.Action.BROWSE;
58
import static java.awt.Desktop.getDesktop;
59
import static org.xhtmlrenderer.swing.ImageResourceLoader.NO_OP_REPAINT_LISTENER;
60
61
/**
62
 * HTML preview pane is responsible for rendering an HTML document.
63
 */
64
public final class HTMLPreviewPane extends Pane {
65
  private final static Notifier NOTIFIER = Services.load( Notifier.class );
66
67
  /**
68
   * Suppresses scrolling to the top on every key press.
69
   */
70
  private static class HTMLPanel extends XHTMLPanel {
71
    @Override
72
    public void resetScrollPosition() {
73
    }
74
  }
75
76
  /**
77
   * Suppresses scroll attempts until after the document has loaded.
78
   */
79
  private static final class DocumentEventHandler implements DocumentListener {
80
    private final BooleanProperty mReadyProperty = new SimpleBooleanProperty();
81
82
    public BooleanProperty readyProperty() {
83
      return mReadyProperty;
84
    }
85
86
    @Override
87
    public void documentStarted() {
88
      mReadyProperty.setValue( Boolean.FALSE );
89
    }
90
91
    @Override
92
    public void documentLoaded() {
93
      mReadyProperty.setValue( Boolean.TRUE );
94
    }
95
96
    @Override
97
    public void onLayoutException( final Throwable t ) {
98
    }
99
100
    @Override
101
    public void onRenderException( final Throwable t ) {
102
    }
103
  }
104
105
  /**
106
   * Responsible for ensuring that images are constrained to the panel width
107
   * upon resizing.
108
   */
109
  private final class ResizeListener implements ComponentListener {
110
    @Override
111
    public void componentResized( final ComponentEvent e ) {
112
      setWidth( e );
113
    }
114
115
    @Override
116
    public void componentShown( final ComponentEvent e ) {
117
      setWidth( e );
118
    }
119
120
    @Override
121
    public void componentMoved( final ComponentEvent e ) {
122
    }
123
124
    @Override
125
    public void componentHidden( final ComponentEvent e ) {
126
    }
127
128
    /**
129
     * Sets the width of the {@link HTMLPreviewPane} so that images can be
130
     * scaled to fit. The scale factor is adjusted a bit below the full width
131
     * to prevent the horizontal scrollbar from appearing.
132
     *
133
     * @param e The component that defines the image scaling width.
134
     */
135
    private void setWidth( final ComponentEvent e ) {
136
      final int width = (int) (e.getComponent().getWidth() * .95);
137
      HTMLPreviewPane.this.mImageLoader.widthProperty().set( width );
138
    }
139
  }
140
141
  /**
142
   * Responsible for launching hyperlinks in the system's default browser.
143
   */
144
  private static class HyperlinkListener extends LinkListener {
145
    @Override
146
    public void linkClicked( final BasicPanel panel, final String uri ) {
147
      try {
148
        final var desktop = getDesktop();
149
150
        if( desktop.isSupported( BROWSE ) ) {
151
          desktop.browse( new URI( uri ) );
152
        }
153
      } catch( final Exception e ) {
154
        NOTIFIER.notify( e );
155
      }
156
    }
157
  }
158
159
  /**
160
   * The CSS must be rendered in points (pt) not pixels (px) to avoid blurry
161
   * rendering on some platforms.
162
   */
163
  private final static String HTML_PREFIX = "<!DOCTYPE html>"
164
      + "<html>"
165
      + "<head>"
166
      + "<link rel='stylesheet' href='" +
167
      HTMLPreviewPane.class.getResource( STYLESHEET_PREVIEW ) + "'/>"
168
      + "</head>"
169
      + "<body>";
170
171
  // Provide some extra space at the end for scrolling past the last line.
172
  private final static String HTML_SUFFIX =
173
      "<p style='height=2em'>&nbsp;</p></body></html>";
174
175
  private final static W3CDom W3C_DOM = new W3CDom();
176
  private final static XhtmlNamespaceHandler NS_HANDLER =
177
      new XhtmlNamespaceHandler();
178
179
  private final StringBuilder mHtmlDocument = new StringBuilder( 65536 );
180
  private final int mHtmlPrefixLength;
181
182
  private final HTMLPanel mHtmlRenderer = new HTMLPanel();
183
  private final SwingNode mSwingNode = new SwingNode();
184
  private final JScrollPane mScrollPane = new JScrollPane( mHtmlRenderer );
185
  private final DocumentEventHandler mDocHandler = new DocumentEventHandler();
186
  private final CustomImageLoader mImageLoader = new CustomImageLoader();
187
188
  private Path mPath = DEFAULT_DIRECTORY;
189
190
  /**
191
   * Creates a new preview pane that can scroll to the caret position within the
192
   * document.
193
   */
194
  public HTMLPreviewPane() {
195
    setStyle( "-fx-background-color: white;" );
196
197
    // No need to append same prefix each time the HTML content is updated.
198
    mHtmlDocument.append( HTML_PREFIX );
199
    mHtmlPrefixLength = mHtmlDocument.length();
200
201
    // Inject an SVG renderer that produces high-quality SVG buffered images.
202
    final var factory = new ChainedReplacedElementFactory();
203
    factory.addFactory( new SVGReplacedElementFactory() );
204
    factory.addFactory( new SwingReplacedElementFactory(
205
        NO_OP_REPAINT_LISTENER, mImageLoader ) );
206
207
    final var context = getSharedContext();
208
    context.setReplacedElementFactory( factory );
209
    context.getTextRenderer().setSmoothingThreshold( 0 );
210
211
    mSwingNode.setContent( mScrollPane );
212
    mSwingNode.setCache( true );
213
214
    mHtmlRenderer.addDocumentListener( mDocHandler );
215
    mHtmlRenderer.addComponentListener( new ResizeListener() );
216
217
    // The default mouse click listener attempts navigation within the
218
    // preview panel. We want to usurp that behaviour to open the link in
219
    // a platform-specific browser.
220
    for( final var listener : mHtmlRenderer.getMouseTrackingListeners() ) {
221
      if( !(listener instanceof HoverListener) ) {
222
        mHtmlRenderer.removeMouseTrackingListener( (FSMouseListener) listener );
223
      }
224
    }
225
226
    mHtmlRenderer.addMouseTrackingListener( new HyperlinkListener() );
227
  }
228
229
  /**
230
   * Updates the internal HTML source, loads it into the preview pane, then
231
   * scrolls to the caret position.
232
   *
233
   * @param html The new HTML document to display.
234
   */
235
  public void process( final String html ) {
236
    final Document jsoupDoc = Jsoup.parse( decorate( html ) );
237
    final org.w3c.dom.Document w3cDoc = W3C_DOM.fromJsoup( jsoupDoc );
238
239
    mHtmlRenderer.setDocument( w3cDoc, getBaseUrl(), NS_HANDLER );
240
  }
241
242
  public void clear() {
243
    process( "" );
244
  }
245
246
  /**
247
   * Scrolls to an anchor link. The anchor links are injected when the
248
   * HTML document is created.
249
   *
250
   * @param id The unique anchor link identifier.
251
   */
252
  public void tryScrollTo( final int id ) {
253
    final ChangeListener<Boolean> listener = new ChangeListener<>() {
254
      @Override
255
      public void changed(
256
          final ObservableValue<? extends Boolean> observable,
257
          final Boolean oldValue,
258
          final Boolean newValue ) {
259
        if( newValue ) {
260
          scrollTo( id );
261
262
          mDocHandler.readyProperty().removeListener( this );
263
        }
264
      }
265
    };
266
267
    mDocHandler.readyProperty().addListener( listener );
268
  }
269
270
  /**
271
   * Scrolls to the closest element matching the given identifier without
272
   * waiting for the document to be ready. Be sure the document is ready
273
   * before calling this method.
274
   *
275
   * @param id Paragraph index.
276
   */
277
  public void scrollTo( final int id ) {
278
    if( id < 2 ) {
279
      scrollToTop();
280
    }
281
    else {
282
      Box box = findPrevBox( id );
283
      box = box == null ? findNextBox( id + 1 ) : box;
284
285
      if( box == null ) {
286
        scrollToBottom();
287
      }
288
      else {
289
        scrollTo( box );
290
      }
291
    }
292
  }
293
294
  private Box findPrevBox( final int id ) {
295
    int prevId = id;
296
    Box box = null;
297
298
    while( prevId > 0 && (box = getBoxById( PARAGRAPH_ID_PREFIX + prevId )) == null ) {
299
      prevId--;
300
    }
301
302
    return box;
303
  }
304
305
  private Box findNextBox( final int id ) {
306
    int nextId = id;
307
    Box box = null;
308
309
    while( nextId - id < 5 &&
310
        (box = getBoxById( PARAGRAPH_ID_PREFIX + nextId )) == null ) {
311
      nextId++;
312
    }
313
314
    return box;
315
  }
316
317
  private void scrollTo( final Point point ) {
318
    mHtmlRenderer.scrollTo( point );
319
  }
320
321
  private void scrollTo( final Box box ) {
322
    scrollTo( createPoint( box ) );
323
  }
324
325
  private void scrollToY( final int y ) {
326
    scrollTo( new Point( 0, y ) );
327
  }
328
329
  private void scrollToTop() {
330
    scrollToY( 0 );
331
  }
332
333
  private void scrollToBottom() {
334
    scrollToY( mHtmlRenderer.getHeight() );
335
  }
336
337
  private Box getBoxById( final String id ) {
338
    return getSharedContext().getBoxById( id );
339
  }
340
341
  private String decorate( final String html ) {
342
    // Trim the HTML back to only the prefix.
343
    mHtmlDocument.setLength( mHtmlPrefixLength );
344
345
    // Write the HTML body element followed by closing tags.
346
    return mHtmlDocument.append( html )
347
                        .append( HTML_SUFFIX )
348
                        .toString();
349
  }
350
351
  public Path getPath() {
352
    return mPath;
353
  }
354
355
  public void setPath( final Path path ) {
356
    assert path != null;
357
    mPath = path;
358
  }
359
360
  /**
361
   * Content to embed in a panel.
362
   *
363
   * @return The content to display to the user.
364
   */
365
  public Node getNode() {
366
    return mSwingNode;
367
  }
368
369
  public JScrollPane getScrollPane() {
370
    return mScrollPane;
371
  }
372
373
  public JScrollBar getVerticalScrollBar() {
374
    return getScrollPane().getVerticalScrollBar();
375
  }
376
377
  /**
378
   * Creates a {@link Point} to use as a reference for scrolling to the area
379
   * described by the given {@link Box}. The {@link Box} coordinates are used
380
   * to populate the {@link Point}'s location, with minor adjustments for
381
   * vertical centering.
382
   *
383
   * @param box The {@link Box} that represents a scrolling anchor reference.
384
   * @return A coordinate suitable for scrolling to.
385
   */
386
  private Point createPoint( final Box box ) {
387
    assert box != null;
388
389
    int x = box.getAbsX();
390
391
    // Scroll back up by half the height of the scroll bar to keep the typing
392
    // area within the view port. Otherwise the view port will have jumped too
393
    // high up and the whatever gets typed won't be visible.
394
    int y = Math.max(
395
        box.getAbsY() - (mScrollPane.getVerticalScrollBar().getHeight() / 2),
396
        0 );
397
398
    if( !box.getStyle().isInline() ) {
399
      final var margin = box.getMargin( mHtmlRenderer.getLayoutContext() );
400
      x += margin.left();
401
      y += margin.top();
402
    }
403
404
    return new Point( x, y );
405
  }
406
407
  private String getBaseUrl() {
408
    final Path basePath = getPath();
409
    final Path parent = basePath == null ? null : basePath.getParent();
410
411
    return parent == null ? "" : parent.toUri().toString();
412
  }
413
414
  private SharedContext getSharedContext() {
415
    return mHtmlRenderer.getSharedContext();
416
  }
417
}
1418
A src/main/java/com/scrivenvar/preview/SVGRasterizer.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.Services;
31
import com.scrivenvar.service.events.Notifier;
32
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
33
import org.apache.batik.gvt.renderer.ImageRenderer;
34
import org.apache.batik.transcoder.TranscoderException;
35
import org.apache.batik.transcoder.TranscoderInput;
36
import org.apache.batik.transcoder.TranscoderOutput;
37
import org.apache.batik.transcoder.image.ImageTranscoder;
38
import org.apache.batik.util.XMLResourceDescriptor;
39
import org.w3c.dom.Document;
40
41
import java.awt.*;
42
import java.awt.image.BufferedImage;
43
import java.io.IOException;
44
import java.io.StringReader;
45
import java.net.URL;
46
import java.util.Map;
47
48
import static java.awt.Color.WHITE;
49
import static java.awt.RenderingHints.*;
50
import static java.awt.image.BufferedImage.TYPE_INT_RGB;
51
import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
52
import static org.apache.batik.transcoder.image.ImageTranscoder.KEY_BACKGROUND_COLOR;
53
import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName;
54
55
/**
56
 * Responsible for converting SVG images into rasterized PNG images.
57
 */
58
public class SVGRasterizer {
59
  private final static Notifier NOTIFIER = Services.load( Notifier.class );
60
61
  private final static SAXSVGDocumentFactory mFactory =
62
      new SAXSVGDocumentFactory( getXMLParserClassName() );
63
64
  public final static Map<Object, Object> RENDERING_HINTS = Map.of(
65
      KEY_ANTIALIASING,
66
      VALUE_ANTIALIAS_ON,
67
      KEY_ALPHA_INTERPOLATION,
68
      VALUE_ALPHA_INTERPOLATION_QUALITY,
69
      KEY_COLOR_RENDERING,
70
      VALUE_COLOR_RENDER_QUALITY,
71
      KEY_DITHERING,
72
      VALUE_DITHER_DISABLE,
73
      KEY_FRACTIONALMETRICS,
74
      VALUE_FRACTIONALMETRICS_ON,
75
      KEY_INTERPOLATION,
76
      VALUE_INTERPOLATION_BICUBIC,
77
      KEY_RENDERING,
78
      VALUE_RENDER_QUALITY,
79
      KEY_STROKE_CONTROL,
80
      VALUE_STROKE_PURE,
81
      KEY_TEXT_ANTIALIASING,
82
      VALUE_TEXT_ANTIALIAS_ON
83
  );
84
85
  public final static Image BROKEN_IMAGE_PLACEHOLDER;
86
87
  static {
88
    // A FontAwesome camera icon, cleft asunder.
89
    final String BROKEN_IMAGE_SVG = "<svg height='19pt' viewBox='0 0 25 19' " +
90
        "width='25pt' xmlns='http://www.w3.org/2000/svg'><g " +
91
        "fill='#454545'><path d='m8.042969 11.085938c.332031 1.445312 1" +
92
        ".660156 2.503906 3.214843 2.558593zm0 0'/><path d='m6.792969 9" +
93
        ".621094-.300781.226562.242187.195313c.015625-.144531.03125-.28125" +
94
        ".058594-.421875zm0 0'/><path d='m10.597656.949219-2.511718.207031c-" +
95
        ".777344.066406-1.429688.582031-1.636719 1.292969l-.367188 1.253906-3" +
96
        ".414062.28125c-1.027344.085937-1.792969.949219-1.699219 1.925781l" +
97
        ".976562 10.621094c.089844.976562.996094 1.699219 2.023438 1" +
98
        ".613281l11.710938-.972656-3.117188-2.484375c-.246094.0625-.5.109375-" +
99
        ".765625.132812-2.566406.210938-4.835937-1.597656-5.0625-4.039062-" +
100
        ".023437-.25-.019531-.496094 0-.738281l-.242187-.195313.300781-" +
101
        ".226562c.359375-1.929688 2.039062-3.472656 4.191406-3.652344.207031-" +
102
        ".015625.414063-.015625.617187-.007812l.933594-.707032zm0 0'/><path " +
103
        "d='m10.234375 11.070312 2.964844 2.820313c.144531.015625.285156" +
104
        ".027344.433593.027344 1.890626 0 3.429688-1.460938 3.429688-3.257813" +
105
        " 0-1.792968-1.539062-3.257812-3.429688-3.257812-1.890624 0-3.429687 " +
106
        "1.464844-3.429687 3.257812 0 .140625.011719.277344.03125.410156zm0 " +
107
        "0'/><path d='m14.488281.808594 1.117188 4.554687-1.042969.546875c2" +
108
        ".25.476563 3.84375 2.472656 3.636719 4.714844-.199219 2.191406-2" +
109
        ".050781 3.871094-4.285157 4.039062l2.609376 2.957032 4.4375.371094c1" +
110
        ".03125.085937 1.9375-.640626 2.027343-1.617188l.976563-10.617188c" +
111
        ".089844-.980468-.667969-1.839843-1.699219-1.925781l-3.414063-" +
112
        ".285156-.371093-1.253906c-.207031-.710938-.859375-1.226563-1" +
113
        ".636719-1.289063zm0 0'/></g></svg>";
114
115
    // The width and height cannot be embedded in the SVG above because the
116
    // path element values are relative to the viewBox dimensions.
117
    final int w = 75;
118
    final int h = 75;
119
    Image image;
120
121
    try( final StringReader reader = new StringReader( BROKEN_IMAGE_SVG ) ) {
122
      final String parser = XMLResourceDescriptor.getXMLParserClassName();
123
      final SAXSVGDocumentFactory factory = new SAXSVGDocumentFactory( parser );
124
      final Document document = factory.createDocument( "", reader );
125
126
      image = rasterize( document, w );
127
    } catch( final Exception e ) {
128
      image = new BufferedImage( w, h, TYPE_INT_RGB );
129
      final var graphics = (Graphics2D) image.getGraphics();
130
      graphics.setRenderingHints( RENDERING_HINTS );
131
132
      // Fall back to a (\) symbol.
133
      graphics.setColor( new Color( 204, 204, 204 ) );
134
      graphics.fillRect( 0, 0, w, h );
135
      graphics.setColor( new Color( 255, 204, 204 ) );
136
      graphics.setStroke( new BasicStroke( 4 ) );
137
      graphics.drawOval( w / 4, h / 4, w / 2, h / 2 );
138
      graphics.drawLine( w / 4 + (int) (w / 4 / Math.PI),
139
                         h / 4 + (int) (w / 4 / Math.PI),
140
                         w / 2 + w / 4 - (int) (w / 4 / Math.PI),
141
                         h / 2 + h / 4 - (int) (w / 4 / Math.PI) );
142
    }
143
144
    BROKEN_IMAGE_PLACEHOLDER = image;
145
  }
146
147
  private static class BufferedImageTranscoder extends ImageTranscoder {
148
    private BufferedImage mImage;
149
150
    @Override
151
    public BufferedImage createImage( final int w, final int h ) {
152
      return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB );
153
    }
154
155
    @Override
156
    public void writeImage(
157
        final BufferedImage image, final TranscoderOutput output ) {
158
      mImage = image;
159
    }
160
161
    public Image getImage() {
162
      return mImage;
163
    }
164
165
    @Override
166
    protected ImageRenderer createRenderer() {
167
      final ImageRenderer renderer = super.createRenderer();
168
      final RenderingHints hints = renderer.getRenderingHints();
169
      hints.putAll( RENDERING_HINTS );
170
171
      renderer.setRenderingHints( hints );
172
173
      return renderer;
174
    }
175
  }
176
177
  /**
178
   * Rasterizes the vector graphic file at the given URL. If any exception
179
   * happens, a red circle is returned instead.
180
   *
181
   * @param url   The URL to a vector graphic file, which must include the
182
   *              protocol scheme (such as file:// or https://).
183
   * @param width The number of pixels wide to render the image. The aspect
184
   *              ratio is maintained.
185
   * @return Either the rasterized image upon success or a red circle.
186
   */
187
  public static Image rasterize( final String url, final int width ) {
188
    try {
189
      return rasterize( new URL( url ), width );
190
    } catch( final Exception e ) {
191
      NOTIFIER.notify( e );
192
      return BROKEN_IMAGE_PLACEHOLDER;
193
    }
194
  }
195
196
  /**
197
   * Converts an SVG drawing into a rasterized image that can be drawn on
198
   * a graphics context.
199
   *
200
   * @param url   The path to the image (can be web address).
201
   * @param width Scale the image width to this size (aspect ratio is
202
   *              maintained).
203
   * @return The vector graphic transcoded into a raster image format.
204
   * @throws IOException         Could not read the vector graphic.
205
   * @throws TranscoderException Could not convert the vector graphic to an
206
   *                             instance of {@link Image}.
207
   */
208
  public static Image rasterize( final URL url, final int width )
209
      throws IOException, TranscoderException {
210
    return rasterize(
211
        mFactory.createDocument( url.toString() ), width );
212
  }
213
214
  public static Image rasterize(
215
      final Document svg, final int width ) throws TranscoderException {
216
    final var transcoder = new BufferedImageTranscoder();
217
    final var input = new TranscoderInput( svg );
218
219
    transcoder.addTranscodingHint( KEY_BACKGROUND_COLOR, WHITE );
220
    transcoder.addTranscodingHint( KEY_WIDTH, (float) width );
221
    transcoder.transcode( input, null );
222
223
    return transcoder.getImage();
224
  }
225
}
1226
A src/main/java/com/scrivenvar/preview/SVGReplacedElementFactory.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.Services;
31
import com.scrivenvar.service.events.Notifier;
32
import org.apache.commons.io.FilenameUtils;
33
import org.w3c.dom.Element;
34
import org.xhtmlrenderer.extend.ReplacedElement;
35
import org.xhtmlrenderer.extend.ReplacedElementFactory;
36
import org.xhtmlrenderer.extend.UserAgentCallback;
37
import org.xhtmlrenderer.layout.LayoutContext;
38
import org.xhtmlrenderer.render.BlockBox;
39
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
40
import org.xhtmlrenderer.swing.ImageReplacedElement;
41
42
import java.awt.*;
43
import java.util.LinkedHashMap;
44
import java.util.Map;
45
46
import static com.scrivenvar.preview.SVGRasterizer.rasterize;
47
48
/**
49
 * Responsible for running {@link SVGRasterizer} on SVG images detected within
50
 * a document to transform them into rasterized versions.
51
 */
52
public class SVGReplacedElementFactory
53
    implements ReplacedElementFactory {
54
55
  private final static Notifier sNotifier = Services.load( Notifier.class );
56
57
  /**
58
   * SVG filename extension.
59
   */
60
  private static final String SVG_FILE = "svg";
61
  private static final String HTML_IMAGE = "img";
62
  private static final String HTML_IMAGE_SRC = "src";
63
64
  /**
65
   * Constrain memory.
66
   */
67
  private static final int MAX_CACHED_IMAGES = 100;
68
69
  /**
70
   * Where to put cached image files.
71
   */
72
  private final Map<String, Image> mImageCache = new LinkedHashMap<>() {
73
    @Override
74
    protected boolean removeEldestEntry(
75
        final Map.Entry<String, Image> eldest ) {
76
      return size() > MAX_CACHED_IMAGES;
77
    }
78
  };
79
80
  @Override
81
  public ReplacedElement createReplacedElement(
82
      final LayoutContext c,
83
      final BlockBox box,
84
      final UserAgentCallback uac,
85
      final int cssWidth,
86
      final int cssHeight ) {
87
    final Element e = box.getElement();
88
89
    if( e != null ) {
90
      final String nodeName = e.getNodeName();
91
92
      if( HTML_IMAGE.equals( nodeName ) ) {
93
        final String src = e.getAttribute( HTML_IMAGE_SRC );
94
        final String ext = FilenameUtils.getExtension( src );
95
96
        if( SVG_FILE.equalsIgnoreCase( ext ) ) {
97
          try {
98
            final int width = box.getContentWidth();
99
            final Image image = getImage( src, width );
100
101
            final int w = image.getWidth( null );
102
            final int h = image.getHeight( null );
103
104
            return new ImageReplacedElement( image, w, h );
105
          } catch( final Exception ex ) {
106
            getNotifier().notify( ex );
107
          }
108
        }
109
      }
110
    }
111
112
    return null;
113
  }
114
115
  @Override
116
  public void reset() {
117
  }
118
119
  @Override
120
  public void remove( final Element e ) {
121
  }
122
123
  @Override
124
  public void setFormSubmissionListener( FormSubmissionListener listener ) {
125
  }
126
127
  private Image getImage( final String src, final int width ) {
128
    return mImageCache.computeIfAbsent( src, v -> rasterize( src, width ) );
129
  }
130
131
  private Notifier getNotifier() {
132
    return sNotifier;
133
  }
134
}
1135
A src/main/java/com/scrivenvar/processors/AbstractProcessor.java
1
/*
2
 * Copyright 2020 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
 * @param <T> The type of object to process.
37
 */
38
public abstract class AbstractProcessor<T> implements Processor<T> {
39
40
  /**
41
   * Used while processing the entire chain; null to signify no more links.
42
   */
43
  private final Processor<T> mNext;
44
45
  /**
46
   * Constructs a new default handler with no successor.
47
   */
48
  protected AbstractProcessor() {
49
    this( null );
50
  }
51
52
  /**
53
   * Constructs a new default handler with a given successor.
54
   *
55
   * @param successor The next processor in the chain.
56
   */
57
  public AbstractProcessor( final Processor<T> successor ) {
58
    mNext = successor;
59
  }
60
61
  @Override
62
  public Processor<T> next() {
63
    return mNext;
64
  }
65
66
  /**
67
   * This algorithm is incorrect, but works for the one use case of removing
68
   * the ending HTML Preview Processor from the end of the processor chain.
69
   * The processor chain is immutable so this creates a succession of
70
   * delegators that wrap each processor in the chain, except for the one
71
   * to be removed.
72
   * <p>
73
   * An alternative is to update the {@link ProcessorFactory} with the ability
74
   * to create a processor chain devoid of an {@link HtmlPreviewProcessor}.
75
   * </p>
76
   *
77
   * @param removal The {@link Processor} to remove from the chain.
78
   * @return A delegating processor chain starting from this processor
79
   * onwards with the given processor removed from the chain.
80
   */
81
  @Override
82
  public Processor<T> remove( final Class<? extends Processor<T>> removal ) {
83
    Processor<T> p = this;
84
    final ProcessorDelegator<T> head = new ProcessorDelegator<>( p );
85
    ProcessorDelegator<T> result = head;
86
87
    while( p != null ) {
88
      final Processor<T> next = p.next();
89
90
      if( next != null && next.getClass() != removal ) {
91
        final var delegator = new ProcessorDelegator<>( next );
92
93
        result.setNext( delegator );
94
        result = delegator;
95
      }
96
97
      p = p.next();
98
    }
99
100
    return head;
101
  }
102
103
  private static final class ProcessorDelegator<T>
104
      extends AbstractProcessor<T> {
105
    private final Processor<T> mDelegate;
106
    private Processor<T> mNext;
107
108
    public ProcessorDelegator( final Processor<T> delegate ) {
109
      super( delegate );
110
111
      assert delegate != null;
112
113
      mDelegate = delegate;
114
    }
115
116
    @Override
117
    public T process( T t ) {
118
      return mDelegate.process( t );
119
    }
120
121
    protected void setNext( final Processor<T> next ) {
122
      mNext = next;
123
    }
124
125
    @Override
126
    public Processor<T> next() {
127
      return mNext;
128
    }
129
  }
130
}
1131
A src/main/java/com/scrivenvar/processors/DefinitionProcessor.java
1
/*
2
 * Copyright 2020 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.Map;
31
32
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
33
34
/**
35
 * Processes interpolated string definitions in the document and inserts
36
 * their values into the post-processed text. The default variable syntax is
37
 * {@code $variable$}.
38
 */
39
public class DefinitionProcessor extends AbstractProcessor<String> {
40
41
  private final Map<String, String> mDefinitions;
42
43
  public DefinitionProcessor(
44
      final Processor<String> successor, final Map<String, String> map ) {
45
    super( successor );
46
    mDefinitions = map;
47
  }
48
49
  /**
50
   * Processes the given text document by replacing variables with their values.
51
   *
52
   * @param text The document text that includes variables that should be
53
   *             replaced with values when rendered as HTML.
54
   * @return The text with all variables replaced.
55
   */
56
  @Override
57
  public String process( final String text ) {
58
    return replace( text, getDefinitions() );
59
  }
60
61
  /**
62
   * Returns the map to use for variable substitution.
63
   *
64
   * @return A map of variable names to values.
65
   */
66
  protected Map<String, String> getDefinitions() {
67
    return mDefinitions;
68
  }
69
}
170
A src/main/java/com/scrivenvar/processors/HtmlPreviewProcessor.java
1
/*
2
 * Copyright 2020 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
public class HtmlPreviewProcessor extends AbstractProcessor<String> {
40
41
  // There is only one preview panel.
42
  private static HTMLPreviewPane sHtmlPreviewPane;
43
44
  /**
45
   * Constructs the end of a processing chain.
46
   *
47
   * @param htmlPreviewPane The pane to update with the post-processed document.
48
   */
49
  public HtmlPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) {
50
    sHtmlPreviewPane = htmlPreviewPane;
51
  }
52
53
  /**
54
   * Update the preview panel using HTML from the succession chain.
55
   *
56
   * @param html The document content to render in the preview pane. The HTML
57
   *             should not contain a doctype, head, or body tag, only
58
   *             content to render within the body.
59
   * @return {@code null} to indicate no more processors in the chain.
60
   */
61
  @Override
62
  public String process( final String html ) {
63
    getHtmlPreviewPane().process( html );
64
65
    // No more processing required.
66
    return null;
67
  }
68
69
  private HTMLPreviewPane getHtmlPreviewPane() {
70
    return sHtmlPreviewPane;
71
  }
72
}
173
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
public class IdentityProcessor extends AbstractProcessor<String> {
35
36
  /**
37
   * Passes the link to the super constructor.
38
   *
39
   * @param link The next processor in the chain to use for text processing.
40
   */
41
  public IdentityProcessor( final Processor<String> link ) {
42
    super( link );
43
  }
44
45
  /**
46
   * Returns the given string, modified with "pre" tags.
47
   *
48
   * @param t The string to return, enclosed in "pre" tags.
49
   * @return The value of t wrapped in "pre" tags.
50
   */
51
  @Override
52
  public String process( final String t ) {
53
    return "<pre>" + t + "</pre>";
54
  }
55
}
156
A src/main/java/com/scrivenvar/processors/InlineRProcessor.java
1
/*
2
 * Copyright 2020 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.preferences.UserPreferences;
32
import com.scrivenvar.service.Options;
33
import com.scrivenvar.service.events.Notifier;
34
35
import javax.script.ScriptEngine;
36
import javax.script.ScriptEngineManager;
37
import javax.script.ScriptException;
38
import java.nio.file.Path;
39
import java.util.LinkedHashMap;
40
import java.util.Map;
41
42
import static com.scrivenvar.Constants.STATUS_PARSE_ERROR;
43
import static com.scrivenvar.Messages.get;
44
import static com.scrivenvar.decorators.RVariableDecorator.PREFIX;
45
import static com.scrivenvar.decorators.RVariableDecorator.SUFFIX;
46
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
47
import static java.lang.Math.min;
48
49
/**
50
 * Transforms a document containing R statements into Markdown.
51
 */
52
public final class InlineRProcessor extends DefinitionProcessor {
53
54
  private static final Notifier sNotifier = Services.load( Notifier.class );
55
  private static final Options sOptions = Services.load( Options.class );
56
57
  /**
58
   * Constrain memory when typing new R expressions into the document.
59
   */
60
  private static final int MAX_CACHED_R_STATEMENTS = 512;
61
62
  /**
63
   * Where to put document inline evaluated R expressions.
64
   */
65
  private final Map<String, Object> mEvalCache = new LinkedHashMap<>() {
66
    @Override
67
    protected boolean removeEldestEntry(
68
        final Map.Entry<String, Object> eldest ) {
69
      return size() > MAX_CACHED_R_STATEMENTS;
70
    }
71
  };
72
73
  /**
74
   * Only one editor is open at a time.
75
   */
76
  private static final ScriptEngine ENGINE =
77
      (new ScriptEngineManager()).getEngineByName( "Renjin" );
78
79
  private static final int PREFIX_LENGTH = PREFIX.length();
80
81
  /**
82
   * Constructs a processor capable of evaluating R statements.
83
   *
84
   * @param processor Subsequent link in the processing chain.
85
   * @param map       Resolved definitions map.
86
   */
87
  public InlineRProcessor(
88
      final Processor<String> processor,
89
      final Map<String, String> map ) {
90
    super( processor, map );
91
    init();
92
  }
93
94
  /**
95
   * Initialises the R code so that R can find imported libraries.
96
   */
97
  private void init() {
98
    try {
99
      final Path wd = getWorkingDirectory();
100
      final String dir = wd.toString().replace( '\\', '/' );
101
      final Map<String, String> map = getDefinitions();
102
      map.put( "$application.r.working.directory$", dir );
103
104
      final String bootstrap = getBootstrapScript();
105
106
      if( !bootstrap.isBlank() ) {
107
        eval( replace( bootstrap, map ) );
108
      }
109
    } catch( final Exception ex ) {
110
      getNotifier().notify( ex );
111
    }
112
  }
113
114
  /**
115
   * Evaluates all R statements in the source document and inserts the
116
   * calculated value into the generated document.
117
   *
118
   * @param text The document text that includes variables that should be
119
   *             replaced with values when rendered as HTML.
120
   * @return The generated document with output from all R statements
121
   * substituted with value returned from their execution.
122
   */
123
  @Override
124
  public String process( final String text ) {
125
    final int length = text.length();
126
127
    // The * 2 is a wild guess at the ratio of R statements to the length
128
    // of text produced by those statements.
129
    final StringBuilder sb = new StringBuilder( length * 2 );
130
131
    int prevIndex = 0;
132
    int currIndex = text.indexOf( PREFIX );
133
134
    while( currIndex >= 0 ) {
135
      // Copy everything up to, but not including, an R statement (`r#).
136
      sb.append( text, prevIndex, currIndex );
137
138
      // Jump to the start of the R statement.
139
      prevIndex = currIndex + PREFIX_LENGTH;
140
141
      // Find the statement ending (`), without indexing past the text boundary.
142
      currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
143
144
      // Only evaluate inline R statements that have end delimiters.
145
      if( currIndex > 1 ) {
146
        // Extract the inline R statement to be evaluated.
147
        final String r = text.substring( prevIndex, currIndex );
148
149
        // Pass the R statement into the R engine for evaluation.
150
        try {
151
          final Object result = evalText( r );
152
153
          // Append the string representation of the result into the text.
154
          sb.append( result );
155
        } catch( final Exception e ) {
156
          // If the string couldn't be parsed using R, append the statement
157
          // that failed to parse, instead of its evaluated value.
158
          sb.append( PREFIX ).append( r ).append( SUFFIX );
159
160
          // Tell the user that there was a problem.
161
          getNotifier().notify(
162
              get( STATUS_PARSE_ERROR, e.getMessage(), currIndex )
163
          );
164
        }
165
166
        // Retain the R statement's ending position in the text.
167
        prevIndex = currIndex + 1;
168
      }
169
170
      // Find the start of the next inline R statement.
171
      currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) );
172
    }
173
174
    // Copy from the previous index to the end of the string.
175
    return sb.append( text.substring( min( prevIndex, length ) ) ).toString();
176
  }
177
178
  /**
179
   * Look up an R expression from the cache then return the resulting object.
180
   * If the R expression hasn't been cached, it'll first be evaluated.
181
   *
182
   * @param r The expression to evaluate.
183
   * @return The object resulting from the evaluation.
184
   */
185
  private Object evalText( final String r ) {
186
    return mEvalCache.computeIfAbsent( r, v -> eval( r ) );
187
  }
188
189
  /**
190
   * Evaluate an R expression and return the resulting object.
191
   *
192
   * @param r The expression to evaluate.
193
   * @return The object resulting from the evaluation.
194
   */
195
  private Object eval( final String r ) {
196
    try {
197
      return getScriptEngine().eval( r );
198
    } catch( final ScriptException ex ) {
199
      getNotifier().notify( ex );
200
      return "";
201
    }
202
  }
203
204
  /**
205
   * This will return the given path if not null, otherwise it will return
206
   * the path to the user's directory.
207
   *
208
   * @return A non-null path.
209
   */
210
  private Path getWorkingDirectory() {
211
    return getUserPreferences().getRDirectory().toPath();
212
  }
213
214
  /**
215
   * Loads the R init script from the application's persisted preferences.
216
   *
217
   * @return A non-null String, possibly empty.
218
   */
219
  private String getBootstrapScript() {
220
    return getUserPreferences().getRScript();
221
  }
222
223
  private UserPreferences getUserPreferences() {
224
    return sOptions.getUserPreferences();
225
  }
226
227
  private ScriptEngine getScriptEngine() {
228
    return ENGINE;
229
  }
230
231
  private Notifier getNotifier() {
232
    return sNotifier;
233
  }
234
}
1235
A src/main/java/com/scrivenvar/processors/Processor.java
1
/*
2
 * Copyright 2020 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
 * @param <T> The type of processor to create.
34
 */
35
public interface Processor<T> {
36
37
  /**
38
   * Processes the given content providing a transformation from one document
39
   * format into another. For example, this could convert from XML to text using
40
   * an XSLT processor, or from markdown to HTML.
41
   *
42
   * @param t The type of object to process.
43
   * @return The post-processed document, or null if processing should stop.
44
   */
45
  T process( T t );
46
47
  Processor<T> remove( Class<? extends Processor<T>> processor );
48
49
  /**
50
   * Adds a document processor to call after this processor finishes processing
51
   * the document given to the process method.
52
   *
53
   * @return The processor that should transform the document after this
54
   * instance has finished processing, or {@code null} if this is the last
55
   * processor in the chain.
56
   */
57
  default Processor<T> next() {
58
    return null;
59
  }
60
}
161
A src/main/java/com/scrivenvar/processors/ProcessorFactory.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.processors.markdown.MarkdownProcessor;
34
35
import java.nio.file.Path;
36
import java.util.Map;
37
38
/**
39
 * Responsible for creating processors capable of parsing, transforming,
40
 * interpolating, and rendering known file types.
41
 */
42
public class ProcessorFactory extends AbstractFileFactory {
43
44
  private final HTMLPreviewPane mPreviewPane;
45
  private final Map<String, String> mResolvedMap;
46
  private final Processor<String> mMarkdownProcessor;
47
48
  /**
49
   * Constructs a factory with the ability to create processors that can perform
50
   * text and caret processing to generate a final preview.
51
   *
52
   * @param previewPane Where the final output is rendered.
53
   * @param resolvedMap Flat map of definitions to replace before final render.
54
   */
55
  public ProcessorFactory(
56
      final HTMLPreviewPane previewPane,
57
      final Map<String, String> resolvedMap ) {
58
    mPreviewPane = previewPane;
59
    mResolvedMap = resolvedMap;
60
    mMarkdownProcessor = createMarkdownProcessor();
61
  }
62
63
  /**
64
   * Creates a processor chain suitable for parsing and rendering the file
65
   * opened at the given tab.
66
   *
67
   * @param tab The tab containing a text editor, path, and caret position.
68
   * @return A processor that can render the given tab's text.
69
   */
70
  public Processor<String> createProcessors( final FileEditorTab tab ) {
71
    final Path path = tab.getPath();
72
    final Processor<String> processor;
73
74
    switch( lookup( path ) ) {
75
      case RMARKDOWN:
76
        processor = createRProcessor();
77
        break;
78
79
      case SOURCE:
80
        processor = createMarkdownDefinitionProcessor();
81
        break;
82
83
      case XML:
84
        processor = createXMLProcessor( tab );
85
        break;
86
87
      case RXML:
88
        processor = createRXMLProcessor( tab );
89
        break;
90
91
      default:
92
        processor = createIdentityProcessor();
93
        break;
94
    }
95
96
    return processor;
97
  }
98
99
  private Processor<String> createHTMLPreviewProcessor() {
100
    return new HtmlPreviewProcessor( getPreviewPane() );
101
  }
102
103
  /**
104
   * Creates and links the processors at the end of the processing chain.
105
   *
106
   * @return A markdown, caret replacement, and preview pane processor chain.
107
   */
108
  private Processor<String> createMarkdownProcessor() {
109
    final var hpp = createHTMLPreviewProcessor();
110
    return new MarkdownProcessor( hpp, getPreviewPane().getPath() );
111
  }
112
113
  protected Processor<String> createIdentityProcessor() {
114
    final var hpp = createHTMLPreviewProcessor();
115
    return new IdentityProcessor( hpp );
116
  }
117
118
  protected Processor<String> createDefinitionProcessor(
119
      final Processor<String> p ) {
120
    return new DefinitionProcessor( p, getResolvedMap() );
121
  }
122
123
  protected Processor<String> createMarkdownDefinitionProcessor() {
124
    final var tpc = getCommonProcessor();
125
    return createDefinitionProcessor( tpc );
126
  }
127
128
  protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
129
    final var tpc = getCommonProcessor();
130
    final var xmlp = new XmlProcessor( tpc, tab.getPath() );
131
    return createDefinitionProcessor( xmlp );
132
  }
133
134
  protected Processor<String> createRProcessor() {
135
    final var tpc = getCommonProcessor();
136
    final var rp = new InlineRProcessor( tpc, getResolvedMap() );
137
    return new RVariableProcessor( rp, getResolvedMap() );
138
  }
139
140
  protected Processor<String> createRXMLProcessor( final FileEditorTab tab ) {
141
    final var tpc = getCommonProcessor();
142
    final var xmlp = new XmlProcessor( tpc, tab.getPath() );
143
    final var rp = new InlineRProcessor( xmlp, getResolvedMap() );
144
    return new RVariableProcessor( rp, getResolvedMap() );
145
  }
146
147
  private HTMLPreviewPane getPreviewPane() {
148
    return mPreviewPane;
149
  }
150
151
  /**
152
   * Returns the variable map of interpolated definitions.
153
   *
154
   * @return A map to help dereference variables.
155
   */
156
  private Map<String, String> getResolvedMap() {
157
    return mResolvedMap;
158
  }
159
160
  /**
161
   * Returns a processor common to all processors: markdown, caret position
162
   * token replacer, and an HTML preview renderer.
163
   *
164
   * @return Processors at the end of the processing chain.
165
   */
166
  private Processor<String> getCommonProcessor() {
167
    return mMarkdownProcessor;
168
  }
169
}
1170
A src/main/java/com/scrivenvar/processors/RVariableProcessor.java
1
/*
2
 * Copyright 2020 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}.
37
 */
38
public class RVariableProcessor extends DefinitionProcessor {
39
40
  public RVariableProcessor(
41
      final Processor<String> rp, final Map<String, String> map ) {
42
    super( rp, map );
43
  }
44
45
  /**
46
   * Returns the R-based version of the interpolated variable definitions.
47
   *
48
   * @return Variable names transmogrified from the default syntax to R syntax.
49
   */
50
  @Override
51
  protected Map<String, String> getDefinitions() {
52
    return toR( super.getDefinitions() );
53
  }
54
55
  /**
56
   * Converts the given map from regular variables to R variables.
57
   *
58
   * @param map Map of variable names to values.
59
   * @return Map of R variables.
60
   */
61
  private Map<String, String> toR( final Map<String, String> map ) {
62
    final Map<String, String> rMap = new HashMap<>( map.size() );
63
64
    for( final Map.Entry<String, String> entry : map.entrySet() ) {
65
      final var key = entry.getKey();
66
      rMap.put( toRKey( key ), toRValue( map.get( key ) ) );
67
    }
68
69
    return rMap;
70
  }
71
72
  /**
73
   * Transforms a variable name from $tree.branch.leaf$ to v$tree$branch$leaf
74
   * form.
75
   *
76
   * @param key The variable name to transform, can be empty but not null.
77
   * @return The transformed variable name.
78
   */
79
  private String toRKey( final String key ) {
80
    // Replace all the periods with dollar symbols.
81
    final StringBuilder sb = new StringBuilder( 'v' + key );
82
    final int length = sb.length();
83
84
    // Replace all periods with dollar symbols. Normally we'd check i >= 0,
85
    // but the prepended 'v' is always going to be a 'v', not a dot.
86
    for( int i = length - 1; i > 0; i-- ) {
87
      if( sb.charAt( i ) == '.' ) {
88
        sb.setCharAt( i, '$' );
89
      }
90
    }
91
92
    // The length is always at least 1 (the 'v'), so bounds aren't broken here.
93
    sb.setLength( length - 1 );
94
95
    return sb.toString();
96
  }
97
98
  private String toRValue( final String value ) {
99
    return '\'' + escape( value, '\'', "\\'" ) + '\'';
100
  }
101
102
  /**
103
   * TODO: Make generic method for replacing text.
104
   *
105
   * @param haystack Search this string for the needle, must not be null.
106
   * @param needle   The character to find in the haystack.
107
   * @param thread   Replace the needle with this text, if the needle is found.
108
   * @return The haystack with the all instances of needle replaced with thread.
109
   */
110
  @SuppressWarnings("SameParameterValue")
111
  private String escape(
112
      final String haystack, final char needle, final String thread ) {
113
    int end = haystack.indexOf( needle );
114
115
    if( end < 0 ) {
116
      return haystack;
117
    }
118
119
    final int length = haystack.length();
120
    int start = 0;
121
122
    // Replace up to 32 occurrences before the string reallocates its buffer.
123
    final StringBuilder sb = new StringBuilder( length + 32 );
124
125
    while( end >= 0 ) {
126
      sb.append( haystack, start, end ).append( thread );
127
      start = end + 1;
128
      end = haystack.indexOf( needle, start );
129
    }
130
131
    return sb.append( haystack.substring( start ) ).toString();
132
  }
133
}
1134
A src/main/java/com/scrivenvar/processors/XmlProcessor.java
1
/*
2
 * Copyright 2020 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 net.sf.saxon.TransformerFactoryImpl;
33
import net.sf.saxon.trans.XPathException;
34
35
import javax.xml.stream.XMLEventReader;
36
import javax.xml.stream.XMLInputFactory;
37
import javax.xml.stream.XMLStreamException;
38
import javax.xml.stream.events.ProcessingInstruction;
39
import javax.xml.stream.events.XMLEvent;
40
import javax.xml.transform.*;
41
import javax.xml.transform.stream.StreamResult;
42
import javax.xml.transform.stream.StreamSource;
43
import java.io.File;
44
import java.io.Reader;
45
import java.io.StringReader;
46
import java.io.StringWriter;
47
import java.nio.file.Path;
48
import java.nio.file.Paths;
49
50
import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute;
51
52
/**
53
 * Transforms an XML document. The XML document must have a stylesheet specified
54
 * as part of its processing instructions, such as:
55
 * <p>
56
 * {@code xml-stylesheet type="text/xsl" href="markdown.xsl"}
57
 * </p>
58
 * <p>
59
 * The XSL must transform the XML document into Markdown, or another format
60
 * recognized by the next link on the chain.
61
 * </p>
62
 */
63
public class XmlProcessor extends AbstractProcessor<String>
64
    implements ErrorListener {
65
66
  private final Snitch snitch = Services.load( Snitch.class );
67
68
  private XMLInputFactory xmlInputFactory;
69
  private TransformerFactory transformerFactory;
70
  private Transformer transformer;
71
72
  private Path path;
73
74
  /**
75
   * Constructs an XML processor that can transform an XML document into another
76
   * format based on the XSL file specified as a processing instruction. The
77
   * path must point to the directory where the XSL file is found, which implies
78
   * that they must be in the same directory.
79
   *
80
   * @param processor Next link in the processing chain.
81
   * @param path      The path to the XML file content to be processed.
82
   */
83
  public XmlProcessor( final Processor<String> processor, final Path path ) {
84
    super( processor );
85
    setPath( path );
86
  }
87
88
  /**
89
   * Transforms the given XML text into another form (typically Markdown).
90
   *
91
   * @param text The text to transform, can be empty, cannot be null.
92
   * @return The transformed text, or empty if text is empty.
93
   */
94
  @Override
95
  public String process( final String text ) {
96
    try {
97
      return text.isEmpty() ? text : transform( text );
98
    } catch( final Exception ex ) {
99
      throw new RuntimeException( ex );
100
    }
101
  }
102
103
  /**
104
   * Performs an XSL transformation on the given XML text. The XML text must
105
   * have a processing instruction that points to the XSL template file to use
106
   * for the transformation.
107
   *
108
   * @param text The text to transform.
109
   * @return The transformed text.
110
   */
111
  private String transform( final String text ) throws Exception {
112
    // Extract the XML stylesheet processing instruction.
113
    final String template = getXsltFilename( text );
114
    final Path xsl = getXslPath( template );
115
116
    try(
117
        final StringWriter output = new StringWriter( text.length() );
118
        final StringReader input = new StringReader( text ) ) {
119
120
      // Listen for external file modification events.
121
      getSnitch().listen( xsl );
122
123
      getTransformer( xsl ).transform(
124
          new StreamSource( input ),
125
          new StreamResult( output )
126
      );
127
128
      return output.toString();
129
    }
130
  }
131
132
  /**
133
   * Returns an XSL transformer ready to transform an XML document using the
134
   * XSLT file specified by the given path. If the path is already known then
135
   * this will return the associated transformer.
136
   *
137
   * @param xsl The path to an XSLT file.
138
   * @return A transformer that will transform XML documents using the given
139
   * XSLT file.
140
   * @throws TransformerConfigurationException Could not instantiate the
141
   *                                           transformer.
142
   */
143
  private Transformer getTransformer( final Path xsl )
144
      throws TransformerConfigurationException {
145
    if( this.transformer == null ) {
146
      this.transformer = createTransformer( xsl );
147
    }
148
149
    return this.transformer;
150
  }
151
152
  /**
153
   * Creates a configured transformer ready to run.
154
   *
155
   * @param xsl The stylesheet to use for transforming XML documents.
156
   * @return The edited XML document transformed into another format (usually
157
   * markdown).
158
   * @throws TransformerConfigurationException Could not create the transformer.
159
   */
160
  protected Transformer createTransformer( final Path xsl )
161
      throws TransformerConfigurationException {
162
    final Source xslt = new StreamSource( xsl.toFile() );
163
164
    return getTransformerFactory().newTransformer( xslt );
165
  }
166
167
  private Path getXslPath( final String filename ) {
168
    final Path xmlPath = getPath();
169
    final File xmlDirectory = xmlPath.toFile().getParentFile();
170
171
    return Paths.get( xmlDirectory.getPath(), filename );
172
  }
173
174
  /**
175
   * Given XML text, this will use a StAX pull reader to obtain the XML
176
   * stylesheet processing instruction. This will throw a parse exception if the
177
   * href pseudo-attribute filename value cannot be found.
178
   *
179
   * @param xml The XML containing an xml-stylesheet processing instruction.
180
   * @return The href pseudo-attribute value.
181
   * @throws XMLStreamException Could not parse the XML file.
182
   */
183
  private String getXsltFilename( final String xml )
184
      throws XMLStreamException, XPathException {
185
186
    String result = "";
187
188
    try( final StringReader sr = new StringReader( xml ) ) {
189
      boolean found = false;
190
      int count = 0;
191
      final XMLEventReader reader = createXMLEventReader( sr );
192
193
      // If the processing instruction wasn't found in the first 10 lines,
194
      // fail fast. This should iterate twice through the loop.
195
      while( !found && reader.hasNext() && count++ < 10 ) {
196
        final XMLEvent event = reader.nextEvent();
197
198
        if( event.isProcessingInstruction() ) {
199
          final ProcessingInstruction pi = (ProcessingInstruction) event;
200
          final String target = pi.getTarget();
201
202
          if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
203
            result = getPseudoAttribute( pi.getData(), "href" );
204
            found = true;
205
          }
206
        }
207
      }
208
    }
209
210
    return result;
211
  }
212
213
  private XMLEventReader createXMLEventReader( final Reader reader )
214
      throws XMLStreamException {
215
    return getXMLInputFactory().createXMLEventReader( reader );
216
  }
217
218
  private synchronized XMLInputFactory getXMLInputFactory() {
219
    if( this.xmlInputFactory == null ) {
220
      this.xmlInputFactory = createXMLInputFactory();
221
    }
222
223
    return this.xmlInputFactory;
224
  }
225
226
  private XMLInputFactory createXMLInputFactory() {
227
    return XMLInputFactory.newInstance();
228
  }
229
230
  private synchronized TransformerFactory getTransformerFactory() {
231
    if( this.transformerFactory == null ) {
232
      this.transformerFactory = createTransformerFactory();
233
    }
234
235
    return this.transformerFactory;
236
  }
237
238
  /**
239
   * Returns a high-performance XSLT 2 transformation engine.
240
   *
241
   * @return An XSL transforming engine.
242
   */
243
  private TransformerFactory createTransformerFactory() {
244
    final TransformerFactory factory = new TransformerFactoryImpl();
245
246
    // Bubble problems up to the user interface, rather than standard error.
247
    factory.setErrorListener( this );
248
249
    return factory;
250
  }
251
252
  /**
253
   * Called when the XSL transformer issues a warning.
254
   *
255
   * @param ex The problem the transformer encountered.
256
   */
257
  @Override
258
  public void warning( final TransformerException ex ) {
259
    throw new RuntimeException( ex );
260
  }
261
262
  /**
263
   * Called when the XSL transformer issues an error.
264
   *
265
   * @param ex The problem the transformer encountered.
266
   */
267
  @Override
268
  public void error( final TransformerException ex ) {
269
    throw new RuntimeException( ex );
270
  }
271
272
  /**
273
   * Called when the XSL transformer issues a fatal error, which is probably
274
   * a bit over-dramatic a method name.
275
   *
276
   * @param ex The problem the transformer encountered.
277
   */
278
  @Override
279
  public void fatalError( final TransformerException ex ) {
280
    throw new RuntimeException( ex );
281
  }
282
283
  private void setPath( final Path path ) {
284
    this.path = path;
285
  }
286
287
  private Path getPath() {
288
    return this.path;
289
  }
290
291
  private Snitch getSnitch() {
292
    return this.snitch;
293
  }
294
}
1295
A src/main/java/com/scrivenvar/processors/markdown/BlockExtension.java
1
package com.scrivenvar.processors.markdown;
2
3
import com.vladsch.flexmark.ast.BlockQuote;
4
import com.vladsch.flexmark.ast.ListBlock;
5
import com.vladsch.flexmark.html.AttributeProvider;
6
import com.vladsch.flexmark.html.AttributeProviderFactory;
7
import com.vladsch.flexmark.html.HtmlRenderer;
8
import com.vladsch.flexmark.html.IndependentAttributeProviderFactory;
9
import com.vladsch.flexmark.html.renderer.AttributablePart;
10
import com.vladsch.flexmark.html.renderer.LinkResolverContext;
11
import com.vladsch.flexmark.util.ast.Block;
12
import com.vladsch.flexmark.util.ast.Node;
13
import com.vladsch.flexmark.util.data.MutableDataHolder;
14
import com.vladsch.flexmark.util.html.MutableAttributes;
15
import org.jetbrains.annotations.NotNull;
16
17
import static com.scrivenvar.Constants.PARAGRAPH_ID_PREFIX;
18
import static com.vladsch.flexmark.html.renderer.CoreNodeRenderer.CODE_CONTENT;
19
20
/**
21
 * Responsible for giving most block-level elements a unique identifier
22
 * attribute. The identifier is used to coordinate scrolling.
23
 */
24
public class BlockExtension implements HtmlRenderer.HtmlRendererExtension {
25
  /**
26
   * Responsible for creating the id attribute. This class is instantiated
27
   * each time the document is rendered, thereby resetting the count to zero.
28
   */
29
  public static class IdAttributeProvider implements AttributeProvider {
30
    private int mCount;
31
32
    private static AttributeProviderFactory createFactory() {
33
      return new IndependentAttributeProviderFactory() {
34
        @Override
35
        public @NotNull AttributeProvider apply(
36
            @NotNull final LinkResolverContext context ) {
37
          return new IdAttributeProvider();
38
        }
39
      };
40
    }
41
42
    @Override
43
    public void setAttributes( @NotNull Node node,
44
                               @NotNull AttributablePart part,
45
                               @NotNull MutableAttributes attributes ) {
46
      // Blockquotes are troublesome because they can interleave blank lines
47
      // without having an equivalent blank line in the source document. That
48
      // is, in Markdown the > symbol on a line by itself will generate a blank
49
      // line in the resulting document; however, a > symbol in the text editor
50
      // does not count as a blank line. Resolving this issue is tricky.
51
      //
52
      // The CODE_CONTENT represents <code> embedded inside <pre>; both elements
53
      // enter this method as FencedCodeBlock, but only the <pre> must be
54
      // uniquely identified (because they are the same line in Markdown).
55
      //
56
      if( node instanceof Block &&
57
          !(node instanceof BlockQuote) &&
58
          !(node instanceof ListBlock) &&
59
          (part != CODE_CONTENT) ) {
60
        attributes.addValue( "id", PARAGRAPH_ID_PREFIX + mCount++ );
61
      }
62
    }
63
  }
64
65
  private BlockExtension() {
66
  }
67
68
  @Override
69
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
70
  }
71
72
  @Override
73
  public void extend( final HtmlRenderer.Builder rendererBuilder,
74
                      @NotNull final String rendererType ) {
75
    rendererBuilder.attributeProviderFactory(
76
        IdAttributeProvider.createFactory()
77
    );
78
  }
79
80
  public static BlockExtension create() {
81
    return new BlockExtension();
82
  }
83
}
184
A src/main/java/com/scrivenvar/processors/markdown/ImageLinkExtension.java
1
/*
2
 * Copyright 2020 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.markdown;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.preferences.UserPreferences;
32
import com.scrivenvar.service.Options;
33
import com.scrivenvar.service.events.Notifier;
34
import com.scrivenvar.util.ProtocolResolver;
35
import com.vladsch.flexmark.ast.Image;
36
import com.vladsch.flexmark.html.HtmlRenderer;
37
import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
38
import com.vladsch.flexmark.html.LinkResolver;
39
import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
40
import com.vladsch.flexmark.html.renderer.LinkStatus;
41
import com.vladsch.flexmark.html.renderer.ResolvedLink;
42
import com.vladsch.flexmark.util.ast.Node;
43
import com.vladsch.flexmark.util.data.MutableDataHolder;
44
import org.jetbrains.annotations.NotNull;
45
import org.renjin.repackaged.guava.base.Splitter;
46
47
import java.io.File;
48
import java.io.FileNotFoundException;
49
import java.nio.file.Path;
50
51
import static java.lang.String.format;
52
53
/**
54
 * Responsible for ensuring that images can be rendered relative to a path.
55
 * This allows images to be located virtually anywhere.
56
 */
57
public class ImageLinkExtension implements HtmlRenderer.HtmlRendererExtension {
58
  /**
59
   * Used for image directory preferences.
60
   */
61
  private final static Options sOptions = Services.load( Options.class );
62
  private final static Notifier sNotifier = Services.load( Notifier.class );
63
64
  /**
65
   * Creates an extension capable of using a relative path to embed images.
66
   *
67
   * @param path The {@link Path} to the file being edited; the parent path
68
   *             is the starting location of the relative image directory.
69
   * @return The new {@link ImageLinkExtension}, never {@code null}.
70
   */
71
  public static ImageLinkExtension create( @NotNull final Path path ) {
72
    return new ImageLinkExtension( path );
73
  }
74
75
  private class Factory extends IndependentLinkResolverFactory {
76
    @Override
77
    public @NotNull LinkResolver apply(
78
        @NotNull final LinkResolverBasicContext context ) {
79
      return new ImageLinkResolver();
80
    }
81
  }
82
83
  private class ImageLinkResolver implements LinkResolver {
84
    private final UserPreferences mUserPref = getUserPreferences();
85
    private final File mImagesUserPrefix = mUserPref.getImagesDirectory();
86
    private final String mImageExtensions = mUserPref.getImagesOrder();
87
88
    public ImageLinkResolver() {
89
    }
90
91
    /**
92
     * You can also set/clear/modify attributes through
93
     * {@link ResolvedLink#getAttributes()} and
94
     * {@link ResolvedLink#getNonNullAttributes()}.
95
     */
96
    @NotNull
97
    @Override
98
    public ResolvedLink resolveLink(
99
        @NotNull final Node node,
100
        @NotNull final LinkResolverBasicContext context,
101
        @NotNull final ResolvedLink link ) {
102
      return node instanceof Image ? resolve( link ) : link;
103
    }
104
105
    private ResolvedLink resolve( final ResolvedLink link ) {
106
      String url = link.getUrl();
107
      final String protocol = ProtocolResolver.getProtocol( url );
108
109
      try {
110
        // If the direct file name exists, then use it directly.
111
        if( ("file".equals( protocol ) && Path.of( url ).toFile().exists()) ||
112
            protocol.startsWith( "http" ) ) {
113
          return valid( link, url );
114
        }
115
      } catch( final Exception ignored ) {
116
        // Try to resolve the image, dynamically.
117
      }
118
119
      try {
120
        final Path imagePrefix = getImagePrefix().toPath();
121
122
        // Path to the file being edited.
123
        Path editPath = getEditPath();
124
125
        // If there is no parent path to the file, it means the file has not
126
        // been saved. Default to using the value from the user's preferences.
127
        // The user's preferences will be defaulted to a the application's
128
        // starting directory.
129
        if( editPath == null ) {
130
          editPath = imagePrefix;
131
        }
132
        else {
133
          editPath = Path.of( editPath.toString(), imagePrefix.toString() );
134
        }
135
136
        final Path imagePathPrefix = Path.of( editPath.toString(), url );
137
        final String suffixes = getImageExtensions();
138
        boolean missing = true;
139
140
        // Iterate over the user's preferred image file type extensions.
141
        for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) {
142
          final String imagePath = format( "%s.%s", imagePathPrefix, ext );
143
          final File file = new File( imagePath );
144
145
          if( file.exists() ) {
146
            url = file.toString();
147
            missing = false;
148
            break;
149
          }
150
        }
151
152
        if( missing ) {
153
          throw new FileNotFoundException( imagePathPrefix + ".*" );
154
        }
155
156
        if( "file".equals( protocol ) ) {
157
          url = "file://" + url;
158
        }
159
160
        getNotifier().clear();
161
162
        return valid( link, url );
163
      } catch( final Exception e ) {
164
        getNotifier().notify( "File not found: " + e.getLocalizedMessage() );
165
      }
166
167
      return link;
168
    }
169
170
    private ResolvedLink valid( final ResolvedLink link, final String url ) {
171
      return link.withStatus( LinkStatus.VALID ).withUrl( url );
172
    }
173
174
    private File getImagePrefix() {
175
      return mImagesUserPrefix;
176
    }
177
178
    private String getImageExtensions() {
179
      return mImageExtensions;
180
    }
181
182
    private Path getEditPath() {
183
      return mPath.getParent();
184
    }
185
  }
186
187
  private final Path mPath;
188
189
  private ImageLinkExtension( @NotNull final Path path ) {
190
    mPath = path;
191
  }
192
193
  @Override
194
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
195
  }
196
197
  @Override
198
  public void extend(
199
      final HtmlRenderer.Builder rendererBuilder,
200
      @NotNull final String rendererType ) {
201
    rendererBuilder.linkResolverFactory( new Factory() );
202
  }
203
204
  private UserPreferences getUserPreferences() {
205
    return getOptions().getUserPreferences();
206
  }
207
208
  private Options getOptions() {
209
    return sOptions;
210
  }
211
212
  private Notifier getNotifier() {
213
    return sNotifier;
214
  }
215
}
1216
A src/main/java/com/scrivenvar/processors/markdown/MarkdownProcessor.java
1
/*
2
 * Copyright 2020 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.markdown;
29
30
import com.scrivenvar.processors.AbstractProcessor;
31
import com.scrivenvar.processors.Processor;
32
import com.vladsch.flexmark.ext.definition.DefinitionExtension;
33
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
34
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
35
import com.vladsch.flexmark.ext.tables.TablesExtension;
36
import com.vladsch.flexmark.ext.typographic.TypographicExtension;
37
import com.vladsch.flexmark.html.HtmlRenderer;
38
import com.vladsch.flexmark.parser.Parser;
39
import com.vladsch.flexmark.util.ast.IParse;
40
import com.vladsch.flexmark.util.ast.Node;
41
import com.vladsch.flexmark.util.misc.Extension;
42
43
import java.nio.file.Path;
44
import java.util.ArrayList;
45
import java.util.Collection;
46
47
import static com.scrivenvar.Constants.USER_DIRECTORY;
48
49
/**
50
 * Responsible for parsing a Markdown document and rendering it as HTML.
51
 */
52
public class MarkdownProcessor extends AbstractProcessor<String> {
53
54
  private final HtmlRenderer mRenderer;
55
  private final IParse mParser;
56
57
  public MarkdownProcessor(
58
      final Processor<String> successor ) {
59
    this( successor, Path.of( USER_DIRECTORY ) );
60
  }
61
62
  /**
63
   * Constructs a new Markdown processor that can create HTML documents.
64
   *
65
   * @param successor Usually the HTML Preview Processor.
66
   */
67
  public MarkdownProcessor(
68
      final Processor<String> successor, final Path path ) {
69
    super( successor );
70
71
    // Standard extensions
72
    final Collection<Extension> extensions = new ArrayList<>();
73
    extensions.add( DefinitionExtension.create() );
74
    extensions.add( StrikethroughSubscriptExtension.create() );
75
    extensions.add( SuperscriptExtension.create() );
76
    extensions.add( TablesExtension.create() );
77
    extensions.add( TypographicExtension.create() );
78
79
    // Allows referencing image files via relative paths and dynamic file types.
80
    extensions.add( ImageLinkExtension.create( path ) );
81
    extensions.add( BlockExtension.create() );
82
83
    mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
84
    mParser = Parser.builder().extensions( extensions ).build();
85
  }
86
87
  /**
88
   * Converts the given Markdown string into HTML, without the doctype, html,
89
   * head, and body tags.
90
   *
91
   * @param markdown The string to convert from Markdown to HTML.
92
   * @return The HTML representation of the Markdown document.
93
   */
94
  @Override
95
  public String process( final String markdown ) {
96
    return toHtml( markdown );
97
  }
98
99
  /**
100
   * Returns the AST in the form of a node for the given markdown document. This
101
   * can be used, for example, to determine if a hyperlink exists inside of a
102
   * paragraph.
103
   *
104
   * @param markdown The markdown to convert into an AST.
105
   * @return The markdown AST for the given text (usually a paragraph).
106
   */
107
  public Node toNode( final String markdown ) {
108
    return parse( markdown );
109
  }
110
111
  /**
112
   * Helper method to create an AST given some markdown.
113
   *
114
   * @param markdown The markdown to parse.
115
   * @return The root node of the markdown tree.
116
   */
117
  private Node parse( final String markdown ) {
118
    return getParser().parse( markdown );
119
  }
120
121
  /**
122
   * Converts a string of markdown into HTML.
123
   *
124
   * @param markdown The markdown text to convert to HTML, must not be null.
125
   * @return The markdown rendered as an HTML document.
126
   */
127
  private String toHtml( final String markdown ) {
128
    return getRenderer().render( parse( markdown ) );
129
  }
130
131
  /**
132
   * Creates the Markdown document processor.
133
   *
134
   * @return A Parser that can build an abstract syntax tree.
135
   */
136
  private IParse getParser() {
137
    return mParser;
138
  }
139
140
  private HtmlRenderer getRenderer() {
141
    return mRenderer;
142
  }
143
}
1144
A src/main/java/com/scrivenvar/processors/text/AbstractTextReplacer.java
1
/*
2
 * Copyright 2020 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
 * Responsible for common behaviour across all text replacer implementations.
34
 */
35
public abstract class AbstractTextReplacer implements TextReplacer {
36
37
  /**
38
   * Default (empty) constructor.
39
   */
40
  protected AbstractTextReplacer() {
41
  }
42
43
  protected String[] keys( final Map<String, String> map ) {
44
    return map.keySet().toArray( new String[ 0 ] );
45
  }
46
47
  protected String[] values( final Map<String, String> map ) {
48
    return map.values().toArray( new String[ 0 ] );
49
  }
50
}
151
A src/main/java/com/scrivenvar/processors/text/AhoCorasickReplacer.java
1
/*
2
 * Copyright 2020 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
public class AhoCorasickReplacer extends AbstractTextReplacer {
39
40
  /**
41
   * Default (empty) constructor.
42
   */
43
  protected AhoCorasickReplacer() {
44
  }
45
46
  @Override
47
  public String replace( final String text, final Map<String, String> map ) {
48
    // Create a buffer sufficiently large that re-allocations are minimized.
49
    final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) );
50
51
    // The TrieBuilder should only match whole words and ignore overlaps (there
52
    // shouldn't be any).
53
    final TrieBuilder builder = builder().onlyWholeWords().ignoreOverlaps();
54
55
    for( final String key : keys( map ) ) {
56
      builder.addKeyword( key );
57
    }
58
59
    int index = 0;
60
61
    // Replace all instances with dereferenced variables.
62
    for( final Emit emit : builder.build().parseText( text ) ) {
63
      sb.append( text, index, emit.getStart() );
64
      sb.append( map.get( emit.getKeyword() ) );
65
      index = emit.getEnd() + 1;
66
    }
67
68
    // Add the remainder of the string (contains no more matches).
69
    sb.append( text.substring( index ) );
70
71
    return sb.toString();
72
  }
73
}
174
A src/main/java/com/scrivenvar/processors/text/StringUtilsReplacer.java
1
/*
2
 * Copyright 2020 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
import static org.apache.commons.lang3.StringUtils.replaceEach;
33
34
/**
35
 * Replaces text using Apache's StringUtils.replaceEach method.
36
 */
37
public class StringUtilsReplacer extends AbstractTextReplacer {
38
39
  /**
40
   * Default (empty) constructor.
41
   */
42
  protected StringUtilsReplacer() {
43
  }
44
45
  @Override
46
  public String replace( final String text, final Map<String, String> map ) {
47
    return replaceEach( text, keys( map ), values( map ) );
48
  }
49
}
150
A src/main/java/com/scrivenvar/processors/text/TextReplacementFactory.java
1
/*
2
 * Copyright 2020 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
public final class TextReplacementFactory {
37
38
  private final static TextReplacer APACHE = new StringUtilsReplacer();
39
  private final static TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
40
41
  /**
42
   * Returns a text search/replacement instance that is reasonably optimal for
43
   * the given length of text.
44
   *
45
   * @param length The length of text that requires some search and replacing.
46
   *
47
   * @return A class that can search and replace text with utmost expediency.
48
   */
49
  public static TextReplacer getTextReplacer( final int length ) {
50
    // After about 1,500 characters, the StringUtils implementation is less
51
    // performant than the Aho-Corsick implementation.
52
    //
53
    // See http://stackoverflow.com/a/40836618/59087
54
    return length < 1500 ? APACHE : AHO_CORASICK;
55
  }
56
57
  /**
58
   * Convenience method to instantiate a suitable text replacer algorithm and
59
   * perform a replacement using the given map. At this point, the values should
60
   * be already dereferenced and ready to be substituted verbatim; any
61
   * recursively defined values must have been interpolated previously.
62
   *
63
   * @param text The text containing zero or more variables to replace.
64
   * @param map The map of variables to their dereferenced values.
65
   *
66
   * @return The text with all variables replaced.
67
   */
68
  public static String replace(
69
    final String text, final Map<String, String> map ) {
70
    return getTextReplacer( text.length() ).replace( text, map );
71
  }
72
}
173
A src/main/java/com/scrivenvar/processors/text/TextReplacer.java
1
/*
2
 * Copyright 2020 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
public interface TextReplacer {
36
37
  /**
38
   * Searches through the given text for any of the keys given in the map and
39
   * replaces the keys that appear in the text with the key's corresponding
40
   * value.
41
   *
42
   * @param text The text that contains zero or more keys.
43
   * @param map  The set of keys mapped to replacement values.
44
   * @return The given text with all keys replaced with corresponding values.
45
   */
46
  String replace( String text, Map<String, String> map );
47
}
148
A src/main/java/com/scrivenvar/service/Options.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.preferences.UserPreferences;
31
32
import java.util.prefs.BackingStoreException;
33
import java.util.prefs.Preferences;
34
35
/**
36
 * Responsible for persisting options.
37
 */
38
public interface Options extends Service {
39
40
  /**
41
   * Returns a reference to the persistent settings that may be configured
42
   * through the UI.
43
   *
44
   * @return A valid {@link UserPreferences} instance, never {@code null}.
45
   */
46
  UserPreferences getUserPreferences();
47
48
  /**
49
   * Returns the {@link Preferences} that persist settings that cannot
50
   * be configured via the user interface.
51
   *
52
   * @return A valid {@link Preferences} instance, never {@code null}.
53
   */
54
  Preferences getState();
55
56
  /**
57
   * Stores the key and value into the user preferences to be loaded the next
58
   * time the application is launched.
59
   *
60
   * @param key   Name of the key to persist along with its value.
61
   * @param value Value to associate with the key.
62
   * @throws BackingStoreException Could not persist the change.
63
   */
64
  void put( String key, String value ) throws BackingStoreException;
65
66
  /**
67
   * Retrieves the value for a key in the user preferences.
68
   *
69
   * @param key          Retrieve the value of this key.
70
   * @param defaultValue The value to return in the event that the given key has
71
   *                     no associated value.
72
   * @return The value associated with the key.
73
   */
74
  String get( String key, String defaultValue );
75
76
  /**
77
   * Retrieves the value for a key in the user preferences. This will return
78
   * the empty string if the value cannot be found.
79
   *
80
   * @param key The key to find in the preferences.
81
   * @return A non-null, possibly empty value for the key.
82
   */
83
  String get( String key );
84
}
185
A src/main/java/com/scrivenvar/service/Service.java
1
/*
2
 * Copyright 2020 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
public interface Service {
34
}
135
A src/main/java/com/scrivenvar/service/Settings.java
1
/*
2
 * Copyright 2020 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
public interface Settings extends Service {
37
38
  /**
39
   * Returns a setting property or its default value.
40
   *
41
   * @param property     The property key name to obtain its value.
42
   * @param defaultValue The default value to return iff the property cannot be
43
   *                     found.
44
   * @return The property value for the given property key.
45
   */
46
  String getSetting( String property, String defaultValue );
47
48
  /**
49
   * Returns a setting property or its default value.
50
   *
51
   * @param property     The property key name to obtain its value.
52
   * @param defaultValue The default value to return iff the property cannot be
53
   *                     found.
54
   * @return The property value for the given property key.
55
   */
56
  int getSetting( String property, int defaultValue );
57
58
  /**
59
   * Returns a list of property names that begin with the given prefix. The
60
   * prefix is included in any matching results. This will return keys that
61
   * either match the prefix or start with the prefix followed by a dot ('.').
62
   * For example, a prefix value of <code>the.property.name</code> will likely
63
   * return the expected results, but <code>the.property.name.</code> (note the
64
   * extraneous period) will probably not.
65
   *
66
   * @param prefix The prefix to compare against each property name.
67
   * @return The list of property names that have the given prefix.
68
   */
69
  Iterator<String> getKeys( final String prefix );
70
71
  /**
72
   * Convert the generic list of property objects into strings.
73
   *
74
   * @param property The property value to coerce.
75
   * @param defaults The defaults values to use should the property be unset.
76
   * @return The list of properties coerced from objects to strings.
77
   */
78
  List<String> getStringSettingList( String property, List<String> defaults );
79
80
  /**
81
   * Converts the generic list of property objects into strings.
82
   *
83
   * @param property The property value to coerce.
84
   * @return The list of properties coerced from objects to strings.
85
   */
86
  List<String> getStringSettingList( String property );
87
}
188
A src/main/java/com/scrivenvar/service/Snitch.java
1
/*
2
 * Copyright 2020 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
public interface Snitch extends Service, Runnable {
38
39
  /**
40
   * Adds an observer to the set of observers for this object, provided that it
41
   * is not the same as some observer already in the set. The order in which
42
   * notifications will be delivered to multiple observers is not specified.
43
   *
44
   * @param o The object to receive changed events for when monitored files
45
   *          are changed.
46
   */
47
  void addObserver( Observer o );
48
49
  /**
50
   * Listens for changes to the path. If the path specifies a file, then only
51
   * notifications pertaining to that file are sent. Otherwise, change events
52
   * for the directory that contains the file are sent. This method must allow
53
   * for multiple calls to the same file without incurring additional listeners
54
   * or events.
55
   *
56
   * @param file Send notifications when this file changes, can be null.
57
   * @throws IOException Couldn't create a watcher for the given file.
58
   */
59
  void listen( Path file ) throws IOException;
60
61
  /**
62
   * Removes the given file from the notifications list.
63
   *
64
   * @param file The file to stop monitoring for any changes, can be null.
65
   */
66
  void ignore( Path file );
67
68
  /**
69
   * Stop listening for events.
70
   */
71
  void stop();
72
}
173
A src/main/java/com/scrivenvar/service/events/Notification.java
1
/*
2
 * Copyright 2020 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
public interface Notification {
34
35
  /**
36
   * Alert title.
37
   *
38
   * @return A non-null string to use as alert message title.
39
   */
40
  String getTitle();
41
42
  /**
43
   * Alert message content.
44
   *
45
   * @return A non-null string that contains information for the user.
46
   */
47
  String getContent();
48
}
149
A src/main/java/com/scrivenvar/service/events/Notifier.java
1
/*
2
 * Copyright 2020 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 javafx.scene.control.Alert;
31
import javafx.scene.control.ButtonType;
32
import javafx.stage.Window;
33
34
import java.io.File;
35
import java.io.FileWriter;
36
import java.io.IOException;
37
import java.io.PrintWriter;
38
import java.util.Observer;
39
40
/**
41
 * Provides the application with a uniform way to notify the user of events.
42
 */
43
public interface Notifier {
44
45
  ButtonType YES = ButtonType.YES;
46
  ButtonType NO = ButtonType.NO;
47
  ButtonType CANCEL = ButtonType.CANCEL;
48
49
  /**
50
   * Notifies the user of a problem.
51
   *
52
   * @param message The problem description.
53
   */
54
  void notify( final String message );
55
56
  /**
57
   * Notifies the user about the exception.
58
   *
59
   * @param ex The exception containing a message to show to the user.
60
   */
61
  default void notify( final Exception ex ) {
62
    assert ex != null;
63
64
    log( ex );
65
    notify( ex.getMessage() );
66
  }
67
68
  /**
69
   * Writes the exception to a log file. The log file should be written
70
   * in the System's temporary directory.
71
   *
72
   * @param ex The exception to show in the status bar and log to a file.
73
   */
74
  default void log( final Exception ex ) {
75
    try(
76
        final FileWriter fw = new FileWriter( getLogPath(), true );
77
        final PrintWriter pw = new PrintWriter( fw )
78
    ) {
79
      ex.printStackTrace( pw );
80
    } catch( final IOException ioe ) {
81
      // The notify method will display the message on the status
82
      // bar.
83
    }
84
  }
85
86
  /**
87
   * Returns the fully qualified path to the log file to write to when
88
   * an exception occurs.
89
   *
90
   * @return Location of the log file for writing unexpected exceptions.
91
   */
92
  File getLogPath();
93
94
  /**
95
   * Causes any displayed notifications to disappear.
96
   */
97
  void clear();
98
99
  /**
100
   * Constructs a default alert message text for a modal alert dialog.
101
   *
102
   * @param title   The dialog box message title.
103
   * @param message The dialog box message content (needs formatting).
104
   * @param args    The arguments to the message content that must be formatted.
105
   * @return The message suitable for building a modal alert dialog.
106
   */
107
  Notification createNotification(
108
      String title,
109
      String message,
110
      Object... args );
111
112
  /**
113
   * Creates an alert of alert type error with a message showing the cause of
114
   * the error.
115
   *
116
   * @param parent  Dialog box owner (for modal purposes).
117
   * @param message The error message, title, and possibly more details.
118
   * @return A modal alert dialog box ready to display using showAndWait.
119
   */
120
  Alert createError( Window parent, Notification message );
121
122
  /**
123
   * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
124
   *
125
   * @param parent  Dialog box owner (for modal purposes).
126
   * @param message The message, title, and possibly more details.
127
   * @return A modal alert dialog box ready to display using showAndWait.
128
   */
129
  Alert createConfirmation( Window parent, Notification message );
130
131
  /**
132
   * Adds an observer to the list of objects that receive notifications about
133
   * error messages to be presented to the user.
134
   *
135
   * @param observer The observer instance to notify.
136
   */
137
  void addObserver( Observer observer );
138
}
1139
A src/main/java/com/scrivenvar/service/events/impl/ButtonOrderPane.java
1
/*
2
 * Copyright 2020 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
public class ButtonOrderPane extends DialogPane {
42
43
  @Override
44
  protected Node createButtonBar() {
45
    final ButtonBar node = (ButtonBar)super.createButtonBar();
46
    node.setButtonOrder( getButtonOrder() );
47
    return node;
48
  }
49
50
  private String getButtonOrder() {
51
    return getSetting( "dialog.alert.button.order.windows", BUTTON_ORDER_WINDOWS );
52
  }
53
54
  @SuppressWarnings("SameParameterValue")
55
  private String getSetting( final String key, final String defaultValue ) {
56
    return getSettings().getSetting( key, defaultValue );
57
  }
58
59
  private Settings getSettings() {
60
    return Services.load( Settings.class );
61
  }
62
}
163
A src/main/java/com/scrivenvar/service/events/impl/DefaultNotification.java
1
/*
2
 * Copyright 2020 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
32
import java.text.MessageFormat;
33
34
/**
35
 * Responsible for alerting the user to prominent information.
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 2020 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 com.scrivenvar.service.events.Notifier;
32
import javafx.scene.control.Alert;
33
import javafx.scene.control.Alert.AlertType;
34
import javafx.stage.Window;
35
36
import java.io.File;
37
import java.nio.file.Paths;
38
import java.util.Observable;
39
40
import static com.scrivenvar.Constants.APP_TITLE;
41
import static com.scrivenvar.Constants.STATUS_BAR_OK;
42
import static com.scrivenvar.Messages.get;
43
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
44
import static javafx.scene.control.Alert.AlertType.ERROR;
45
46
/**
47
 * Provides the ability to notify the user of problems.
48
 */
49
public final class DefaultNotifier extends Observable implements Notifier {
50
51
  private final static String OK = get( STATUS_BAR_OK, "OK" );
52
53
  /**
54
   * Notifies all observer instances of the given message.
55
   *
56
   * @param message The text to display to the user.
57
   */
58
  @Override
59
  public void notify( final String message ) {
60
    if( message != null && !message.isBlank() ) {
61
      setChanged();
62
      notifyObservers( message );
63
    }
64
  }
65
66
  @Override
67
  public void clear() {
68
    notify( OK );
69
  }
70
71
  /**
72
   * Contains all the information that the user needs to know about a problem.
73
   *
74
   * @param title   The context for the message.
75
   * @param message The message content (formatted with the given args).
76
   * @param args    Parameters for the message content.
77
   * @return A notification instance, never null.
78
   */
79
  @Override
80
  public Notification createNotification(
81
      final String title,
82
      final String message,
83
      final Object... args ) {
84
    return new DefaultNotification( title, message, args );
85
  }
86
87
  private Alert createAlertDialog(
88
      final Window parent,
89
      final AlertType alertType,
90
      final Notification message ) {
91
92
    final Alert alert = new Alert( alertType );
93
94
    alert.setDialogPane( new ButtonOrderPane() );
95
    alert.setTitle( message.getTitle() );
96
    alert.setHeaderText( null );
97
    alert.setContentText( message.getContent() );
98
    alert.initOwner( parent );
99
100
    return alert;
101
  }
102
103
  @Override
104
  public Alert createConfirmation( final Window parent,
105
                                   final Notification message ) {
106
    final Alert alert = createAlertDialog( parent, CONFIRMATION, message );
107
108
    alert.getButtonTypes().setAll( YES, NO, CANCEL );
109
110
    return alert;
111
  }
112
113
  @Override
114
  public Alert createError( final Window parent, final Notification message ) {
115
    return createAlertDialog( parent, ERROR, message );
116
  }
117
118
  @Override
119
  public File getLogPath() {
120
    return Paths.get(
121
        System.getProperty( "java.io.tmpdir" ), APP_TITLE + ".log" ).toFile();
122
  }
123
}
1124
A src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
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 com.scrivenvar.preferences.UserPreferences;
30
import com.scrivenvar.service.Options;
31
32
import java.util.prefs.BackingStoreException;
33
import java.util.prefs.Preferences;
34
35
import static com.scrivenvar.Constants.PREFS_ROOT;
36
import static com.scrivenvar.Constants.PREFS_STATE;
37
import static java.util.prefs.Preferences.userRoot;
38
39
/**
40
 * Persistent options user can change at runtime.
41
 */
42
public class DefaultOptions implements Options {
43
  private final UserPreferences mPreferences = new UserPreferences();
44
45
  public DefaultOptions() {
46
  }
47
48
  /**
49
   * This will throw IllegalArgumentException if the value exceeds the maximum
50
   * preferences value length.
51
   *
52
   * @param key   The name of the key to associate with the value.
53
   * @param value The value to persist.
54
   * @throws BackingStoreException New value not persisted.
55
   */
56
  @Override
57
  public void put( final String key, final String value )
58
      throws BackingStoreException {
59
    getState().put( key, value );
60
    getState().flush();
61
  }
62
63
  @Override
64
  public String get( final String key, final String value ) {
65
    return getState().get( key, value );
66
  }
67
68
  @Override
69
  public String get( final String key ) {
70
    return get( key, "" );
71
  }
72
73
  private Preferences getRootPreferences() {
74
    return userRoot().node( PREFS_ROOT );
75
  }
76
77
  @Override
78
  public Preferences getState() {
79
    return getRootPreferences().node( PREFS_STATE );
80
  }
81
82
  @Override
83
  public UserPreferences getUserPreferences() {
84
    return mPreferences;
85
  }
86
}
187
A src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.service.Settings;
31
import org.apache.commons.configuration2.PropertiesConfiguration;
32
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
33
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
34
import org.apache.commons.configuration2.ex.ConfigurationException;
35
36
import java.io.IOException;
37
import java.io.InputStreamReader;
38
import java.io.Reader;
39
import java.net.URL;
40
import java.nio.charset.Charset;
41
import java.util.Iterator;
42
import java.util.List;
43
44
import static com.scrivenvar.Constants.SETTINGS_NAME;
45
46
/**
47
 * Responsible for loading settings that help avoid hard-coded assumptions.
48
 */
49
public class DefaultSettings implements Settings {
50
51
  private static final char VALUE_SEPARATOR = ',';
52
53
  private PropertiesConfiguration properties;
54
55
  public DefaultSettings() throws ConfigurationException {
56
    setProperties( createProperties() );
57
  }
58
59
  /**
60
   * Returns the value of a string property.
61
   *
62
   * @param property     The property key.
63
   * @param defaultValue The value to return if no property key has been set.
64
   * @return The property key value, or defaultValue when no key found.
65
   */
66
  @Override
67
  public String getSetting( final String property, final String defaultValue ) {
68
    return getSettings().getString( property, defaultValue );
69
  }
70
71
  /**
72
   * Returns the value of a string property.
73
   *
74
   * @param property     The property key.
75
   * @param defaultValue The value to return if no property key has been set.
76
   * @return The property key value, or defaultValue when no key found.
77
   */
78
  @Override
79
  public int getSetting( final String property, final int defaultValue ) {
80
    return getSettings().getInt( property, defaultValue );
81
  }
82
83
  /**
84
   * Convert the generic list of property objects into strings.
85
   *
86
   * @param property The property value to coerce.
87
   * @param defaults The defaults values to use should the property be unset.
88
   * @return The list of properties coerced from objects to strings.
89
   */
90
  @Override
91
  public List<String> getStringSettingList(
92
      final String property, final List<String> defaults ) {
93
    return getSettings().getList( String.class, property, defaults );
94
  }
95
96
  /**
97
   * Convert a list of property objects into strings, with no default value.
98
   *
99
   * @param property The property value to coerce.
100
   * @return The list of properties coerced from objects to strings.
101
   */
102
  @Override
103
  public List<String> getStringSettingList( final String property ) {
104
    return getStringSettingList( property, null );
105
  }
106
107
  /**
108
   * Returns a list of property names that begin with the given prefix.
109
   *
110
   * @param prefix The prefix to compare against each property name.
111
   * @return The list of property names that have the given prefix.
112
   */
113
  @Override
114
  public Iterator<String> getKeys( final String prefix ) {
115
    return getSettings().getKeys( prefix );
116
  }
117
118
  private PropertiesConfiguration createProperties()
119
      throws ConfigurationException {
120
121
    final URL url = getPropertySource();
122
    final PropertiesConfiguration configuration = new PropertiesConfiguration();
123
124
    if( url != null ) {
125
      try( final Reader r = new InputStreamReader( url.openStream(),
126
                                                   getDefaultEncoding() ) ) {
127
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
128
        configuration.read( r );
129
130
      } catch( final IOException ex ) {
131
        throw new RuntimeException( new ConfigurationException( ex ) );
132
      }
133
    }
134
135
    return configuration;
136
  }
137
138
  protected Charset getDefaultEncoding() {
139
    return Charset.defaultCharset();
140
  }
141
142
  protected ListDelimiterHandler createListDelimiterHandler() {
143
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
144
  }
145
146
  private URL getPropertySource() {
147
    return DefaultSettings.class.getResource( getSettingsFilename() );
148
  }
149
150
  private String getSettingsFilename() {
151
    return SETTINGS_NAME;
152
  }
153
154
  private void setProperties( final PropertiesConfiguration configuration ) {
155
    this.properties = configuration;
156
  }
157
158
  private PropertiesConfiguration getSettings() {
159
    return this.properties;
160
  }
161
}
1162
A src/main/java/com/scrivenvar/service/impl/DefaultSnitch.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.service.Snitch;
31
32
import java.io.IOException;
33
import java.nio.file.*;
34
import java.util.Collections;
35
import java.util.Map;
36
import java.util.Observable;
37
import java.util.Set;
38
import java.util.concurrent.ConcurrentHashMap;
39
40
import static com.scrivenvar.Constants.APP_WATCHDOG_TIMEOUT;
41
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
42
43
/**
44
 * Listens for file changes. Other classes can register paths to be monitored
45
 * and listen for changes to those paths.
46
 */
47
public class DefaultSnitch extends Observable implements Snitch {
48
49
  /**
50
   * Service for listening to directories for modifications.
51
   */
52
  private WatchService watchService;
53
54
  /**
55
   * Directories being monitored for changes.
56
   */
57
  private Map<WatchKey, Path> keys;
58
59
  /**
60
   * Files that will kick off notification events if modified.
61
   */
62
  private Set<Path> eavesdropped;
63
64
  /**
65
   * Set to true when running; set to false to stop listening.
66
   */
67
  private volatile boolean listening;
68
69
  public DefaultSnitch() {
70
  }
71
72
  @Override
73
  public void stop() {
74
    setListening( false );
75
  }
76
77
  /**
78
   * Adds a listener to the list of files to watch for changes. If the file is
79
   * already in the monitored list, this will return immediately.
80
   *
81
   * @param file Path to a file to watch for changes.
82
   * @throws IOException The file could not be monitored.
83
   */
84
  @Override
85
  public void listen( final Path file ) throws IOException {
86
    if( file != null && getEavesdropped().add( file ) ) {
87
      final Path dir = toDirectory( file );
88
      final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY );
89
90
      getWatchMap().put( key, dir );
91
    }
92
  }
93
94
  /**
95
   * Returns the given path to a file (or directory) as a directory. If the
96
   * given path is already a directory, it is returned. Otherwise, this returns
97
   * the directory that contains the file. This will fail if the file is stored
98
   * in the root folder.
99
   *
100
   * @param path The file to return as a directory, which should always be the
101
   *             case.
102
   * @return The given path as a directory, if a file, otherwise the path
103
   * itself.
104
   */
105
  private Path toDirectory( final Path path ) {
106
    return Files.isDirectory( path )
107
        ? path
108
        : path.toFile().getParentFile().toPath();
109
  }
110
111
  /**
112
   * Stop listening to the given file for change events. This fails silently.
113
   *
114
   * @param file The file to no longer monitor for changes.
115
   */
116
  @Override
117
  public void ignore( final Path file ) {
118
    if( file != null ) {
119
      final Path directory = toDirectory( file );
120
121
      // Remove all occurrences (there should be only one).
122
      getWatchMap().values().removeAll( Collections.singleton( directory ) );
123
124
      // Remove all occurrences (there can be only one).
125
      getEavesdropped().remove( file );
126
    }
127
  }
128
129
  /**
130
   * Loops until stop is called, or the application is terminated.
131
   */
132
  @Override
133
  @SuppressWarnings("BusyWait")
134
  public void run() {
135
    setListening( true );
136
137
    while( isListening() ) {
138
      try {
139
        final WatchKey key = getWatchService().take();
140
        final Path path = get( key );
141
142
        // Prevent receiving two separate ENTRY_MODIFY events: file modified
143
        // and timestamp updated. Instead, receive one ENTRY_MODIFY event
144
        // with two counts.
145
        Thread.sleep( APP_WATCHDOG_TIMEOUT );
146
147
        for( final WatchEvent<?> event : key.pollEvents() ) {
148
          final Path changed = path.resolve( (Path) event.context() );
149
150
          if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
151
            setChanged();
152
            notifyObservers( changed );
153
          }
154
        }
155
156
        if( !key.reset() ) {
157
          ignore( path );
158
        }
159
      } catch( final IOException | InterruptedException ex ) {
160
        // Stop eavesdropping.
161
        setListening( false );
162
      }
163
    }
164
  }
165
166
  /**
167
   * Returns true if the list of files being listened to for changes contains
168
   * the given file.
169
   *
170
   * @param file Path to a system file.
171
   * @return true The given file is being monitored for changes.
172
   */
173
  private boolean isListening( final Path file ) {
174
    return getEavesdropped().contains( file );
175
  }
176
177
  /**
178
   * Returns a path for a given watch key.
179
   *
180
   * @param key The key to lookup its corresponding path.
181
   * @return The path for the given key.
182
   */
183
  private Path get( final WatchKey key ) {
184
    return getWatchMap().get( key );
185
  }
186
187
  private synchronized Map<WatchKey, Path> getWatchMap() {
188
    if( this.keys == null ) {
189
      this.keys = createWatchKeys();
190
    }
191
192
    return this.keys;
193
  }
194
195
  protected Map<WatchKey, Path> createWatchKeys() {
196
    return new ConcurrentHashMap<>();
197
  }
198
199
  /**
200
   * Returns a list of files that, when changed, will kick off a notification.
201
   *
202
   * @return A non-null, possibly empty, list of files.
203
   */
204
  private synchronized Set<Path> getEavesdropped() {
205
    if( this.eavesdropped == null ) {
206
      this.eavesdropped = createEavesdropped();
207
    }
208
209
    return this.eavesdropped;
210
  }
211
212
  protected Set<Path> createEavesdropped() {
213
    return ConcurrentHashMap.newKeySet();
214
  }
215
216
  /**
217
   * The existing watch service, or a new instance if null.
218
   *
219
   * @return A valid WatchService instance, never null.
220
   * @throws IOException Could not create a new watch service.
221
   */
222
  private synchronized WatchService getWatchService() throws IOException {
223
    if( this.watchService == null ) {
224
      this.watchService = createWatchService();
225
    }
226
227
    return this.watchService;
228
  }
229
230
  protected WatchService createWatchService() throws IOException {
231
    final FileSystem fileSystem = FileSystems.getDefault();
232
    return fileSystem.newWatchService();
233
  }
234
235
  /**
236
   * Answers whether the loop should continue executing.
237
   *
238
   * @return true The internal listening loop should continue listening for file
239
   * modification events.
240
   */
241
  protected boolean isListening() {
242
    return this.listening;
243
  }
244
245
  /**
246
   * Requests the snitch to stop eavesdropping on file changes.
247
   *
248
   * @param listening Use true to indicate the service should stop running.
249
   */
250
  private void setListening( final boolean listening ) {
251
    this.listening = listening;
252
  }
253
}
1254
A src/main/java/com/scrivenvar/spelling/api/SpellCheckListener.java
1
/*
2
 * Copyright 2020 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.spelling.api;
29
30
import java.util.function.BiConsumer;
31
32
/**
33
 * Represents an operation that accepts two input arguments and returns no
34
 * result. Unlike most other functional interfaces, this class is expected to
35
 * operate via side-effects.
36
 * <p>
37
 * This is used instead of a {@link BiConsumer} to avoid autoboxing.
38
 * </p>
39
 */
40
@FunctionalInterface
41
public interface SpellCheckListener {
42
43
  /**
44
   * Performs an operation on the given arguments.
45
   *
46
   * @param text        The text associated with a beginning and ending offset.
47
   * @param beganOffset A starting offset, used as an index into a string.
48
   * @param endedOffset An ending offset, which should equal text.length() +
49
   *                    beganOffset.
50
   */
51
  void accept( String text, int beganOffset, int endedOffset );
52
}
153
A src/main/java/com/scrivenvar/spelling/api/SpellChecker.java
1
/*
2
 * Copyright 2020 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.spelling.api;
29
30
import java.util.List;
31
32
/**
33
 * Defines the responsibilities for a spell checking API. The intention is
34
 * to allow different spell checking implementations to be used by the
35
 * application, such as SymSpell and LinSpell.
36
 */
37
public interface SpellChecker {
38
39
  /**
40
   * Answers whether the given lexeme, in whole, is found in the lexicon. The
41
   * lexicon lookup is performed case-insensitively. This method should be
42
   * used instead of {@link #suggestions(String, int)} for performance reasons.
43
   *
44
   * @param lexeme The word to check for correctness.
45
   * @return {@code true} if the lexeme is in the lexicon.
46
   */
47
  boolean inLexicon( String lexeme );
48
49
  /**
50
   * Gets a list of spelling corrections for the given lexeme.
51
   *
52
   * @param lexeme A word to check for correctness that's not in the lexicon.
53
   * @param count  The maximum number of alternatives to return.
54
   * @return A list of words in the lexicon that are similar to the given
55
   * lexeme.
56
   */
57
  List<String> suggestions( String lexeme, int count );
58
59
  /**
60
   * Iterates over the given text, emitting starting and ending offsets into
61
   * the text for every word that is missing from the lexicon.
62
   *
63
   * @param text     The text to check for words missing from the lexicon.
64
   * @param consumer Every missing word emits a message with the starting
65
   *                 and ending offset into the text where said word is found.
66
   */
67
  void proofread( String text, SpellCheckListener consumer );
68
}
169
A src/main/java/com/scrivenvar/spelling/impl/PermissiveSpeller.java
1
/*
2
 * Copyright 2020 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.spelling.impl;
29
30
import com.scrivenvar.spelling.api.SpellCheckListener;
31
import com.scrivenvar.spelling.api.SpellChecker;
32
33
import java.util.List;
34
35
/**
36
 * Responsible for spell checking in the event that a real spell checking
37
 * implementation cannot be created (for any reason). Does not perform any
38
 * spell checking and indicates that any given lexeme is in the lexicon.
39
 */
40
public class PermissiveSpeller implements SpellChecker {
41
  /**
42
   * Returns {@code true}, ignoring the given word.
43
   *
44
   * @param ignored Unused.
45
   * @return {@code true}
46
   */
47
  @Override
48
  public boolean inLexicon( final String ignored ) {
49
    return true;
50
  }
51
52
  /**
53
   * Returns an array with the given lexeme.
54
   *
55
   * @param lexeme  The word to return.
56
   * @param ignored Unused.
57
   * @return A suggestion list containing the given lexeme.
58
   */
59
  @Override
60
  public List<String> suggestions( final String lexeme, final int ignored ) {
61
    return List.of( lexeme );
62
  }
63
64
  /**
65
   * Performs no action.
66
   *
67
   * @param text    Unused.
68
   * @param ignored Uncalled.
69
   */
70
  @Override
71
  public void proofread(
72
      final String text, final SpellCheckListener ignored ) {
73
  }
74
}
175
A src/main/java/com/scrivenvar/spelling/impl/SymSpellSpeller.java
1
/*
2
 * Copyright 2020 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.spelling.impl;
29
30
import com.scrivenvar.spelling.api.SpellCheckListener;
31
import com.scrivenvar.spelling.api.SpellChecker;
32
import io.gitlab.rxp90.jsymspell.SuggestItem;
33
import io.gitlab.rxp90.jsymspell.SymSpell;
34
import io.gitlab.rxp90.jsymspell.SymSpellBuilder;
35
36
import java.text.BreakIterator;
37
import java.util.ArrayList;
38
import java.util.Collection;
39
import java.util.List;
40
41
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity;
42
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.ALL;
43
import static io.gitlab.rxp90.jsymspell.SymSpell.Verbosity.CLOSEST;
44
import static java.lang.Character.isLetter;
45
46
/**
47
 * Responsible for spell checking using {@link SymSpell}.
48
 */
49
public class SymSpellSpeller implements SpellChecker {
50
  private final BreakIterator mBreakIterator = BreakIterator.getWordInstance();
51
52
  private final SymSpell mSymSpell;
53
54
  /**
55
   * Creates a new lexicon for the given collection of lexemes.
56
   *
57
   * @param lexiconWords The words in the lexicon to add for spell checking,
58
   *                     must not be empty.
59
   * @return An instance of {@link SpellChecker} that can check if a word
60
   * is correct and suggest alternatives.
61
   */
62
  public static SpellChecker forLexicon(
63
      final Collection<String> lexiconWords ) {
64
    assert lexiconWords != null && !lexiconWords.isEmpty();
65
66
    final SymSpellBuilder builder = new SymSpellBuilder()
67
        .setLexiconWords( lexiconWords );
68
69
    return new SymSpellSpeller( builder.build() );
70
  }
71
72
  /**
73
   * Prevent direct instantiation so that only the {@link SpellChecker}
74
   * interface
75
   * is available.
76
   *
77
   * @param symSpell The implementation-specific spell checker.
78
   */
79
  private SymSpellSpeller( final SymSpell symSpell ) {
80
    mSymSpell = symSpell;
81
  }
82
83
  @Override
84
  public boolean inLexicon( final String lexeme ) {
85
    return lookup( lexeme, CLOSEST ).size() == 1;
86
  }
87
88
  @Override
89
  public List<String> suggestions( final String lexeme, int count ) {
90
    final List<String> result = new ArrayList<>( count );
91
92
    for( final var item : lookup( lexeme, ALL ) ) {
93
      if( count-- > 0 ) {
94
        result.add( item.getSuggestion() );
95
      }
96
      else {
97
        break;
98
      }
99
    }
100
101
    return result;
102
  }
103
104
  @Override
105
  public void proofread(
106
      final String text, final SpellCheckListener consumer ) {
107
    assert text != null;
108
    assert consumer != null;
109
110
    mBreakIterator.setText( text );
111
112
    int boundaryIndex = mBreakIterator.first();
113
    int previousIndex = 0;
114
115
    while( boundaryIndex != BreakIterator.DONE ) {
116
      final var lex = text.substring( previousIndex, boundaryIndex )
117
                          .toLowerCase();
118
119
      // Get the lexeme for the possessive.
120
      final var pos = lex.endsWith( "'s" ) || lex.endsWith( "’s" );
121
      final var lexeme = pos ? lex.substring( 0, lex.length() - 2 ) : lex;
122
123
      if( isWord( lexeme ) && !inLexicon( lexeme ) ) {
124
        consumer.accept( lex, previousIndex, boundaryIndex );
125
      }
126
127
      previousIndex = boundaryIndex;
128
      boundaryIndex = mBreakIterator.next();
129
    }
130
  }
131
132
  /**
133
   * Answers whether the given string is likely a word by checking the first
134
   * character.
135
   *
136
   * @param word The word to check.
137
   * @return {@code true} if the word begins with a letter.
138
   */
139
  private boolean isWord( final String word ) {
140
    return !word.isEmpty() && isLetter( word.charAt( 0 ) );
141
  }
142
143
  /**
144
   * Returns a list of {@link SuggestItem} instances that provide alternative
145
   * spellings for the given lexeme.
146
   *
147
   * @param lexeme A word to look up in the lexicon.
148
   * @param v      Influences the number of results returned.
149
   * @return Alternative lexemes.
150
   */
151
  private List<SuggestItem> lookup( final String lexeme, final Verbosity v ) {
152
    return getSpeller().lookup( lexeme, v );
153
  }
154
155
  private SymSpell getSpeller() {
156
    return mSymSpell;
157
  }
158
}
1159
A src/main/java/com/scrivenvar/util/Action.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
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
 * Defines actions the user can take by interacting with the GUI.
37
 */
38
public class Action {
39
  public final String text;
40
  public final KeyCombination accelerator;
41
  public final GlyphIcons icon;
42
  public final EventHandler<ActionEvent> action;
43
  public final ObservableBooleanValue disable;
44
45
  public Action(
46
      final String text,
47
      final String accelerator,
48
      final GlyphIcons icon,
49
      final EventHandler<ActionEvent> action,
50
      final ObservableBooleanValue disable ) {
51
52
    this.text = text;
53
    this.accelerator = accelerator == null ?
54
        null : KeyCombination.valueOf( accelerator );
55
    this.icon = icon;
56
    this.action = action;
57
    this.disable = disable;
58
  }
59
}
160
A src/main/java/com/scrivenvar/util/ActionBuilder.java
1
/*
2
 * Copyright 2020 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 com.scrivenvar.Messages;
31
import de.jensd.fx.glyphs.GlyphIcons;
32
import javafx.beans.value.ObservableBooleanValue;
33
import javafx.event.ActionEvent;
34
import javafx.event.EventHandler;
35
36
/**
37
 * Provides a fluent interface around constructing actions so that duplication
38
 * can be avoided.
39
 */
40
public class ActionBuilder {
41
  private String mText;
42
  private String mAccelerator;
43
  private GlyphIcons mIcon;
44
  private EventHandler<ActionEvent> mAction;
45
  private ObservableBooleanValue mDisable;
46
47
  /**
48
   * Sets the action text based on a resource bundle key.
49
   *
50
   * @param key The key to look up in the {@link Messages}.
51
   * @return The corresponding value, or the key name if none found.
52
   */
53
  public ActionBuilder setText( final String key ) {
54
    mText = Messages.get( key, key );
55
    return this;
56
  }
57
58
  public ActionBuilder setAccelerator( final String accelerator ) {
59
    mAccelerator = accelerator;
60
    return this;
61
  }
62
63
  public ActionBuilder setIcon( final GlyphIcons icon ) {
64
    mIcon = icon;
65
    return this;
66
  }
67
68
  public ActionBuilder setAction( final EventHandler<ActionEvent> action ) {
69
    mAction = action;
70
    return this;
71
  }
72
73
  public ActionBuilder setDisable( final ObservableBooleanValue disable ) {
74
    mDisable = disable;
75
    return this;
76
  }
77
78
  public Action build() {
79
    return new Action( mText, mAccelerator, mIcon, mAction, mDisable );
80
  }
81
}
182
A src/main/java/com/scrivenvar/util/ActionUtils.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
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
 * Responsible for creating menu items and toolbar buttons.
41
 */
42
public class ActionUtils {
43
44
  public static Menu createMenu( final String text, final Action... actions ) {
45
    return new Menu( text, null, createMenuItems( actions ) );
46
  }
47
48
  public static MenuItem[] createMenuItems( final Action... actions ) {
49
    final MenuItem[] menuItems = new MenuItem[ actions.length ];
50
51
    for( int i = 0; i < actions.length; i++ ) {
52
      menuItems[ i ] = (actions[ i ] == null)
53
          ? new SeparatorMenuItem()
54
          : createMenuItem( actions[ i ] );
55
    }
56
57
    return menuItems;
58
  }
59
60
  public static MenuItem createMenuItem( final Action action ) {
61
    final MenuItem menuItem = new MenuItem( action.text );
62
63
    if( action.accelerator != null ) {
64
      menuItem.setAccelerator( action.accelerator );
65
    }
66
67
    if( action.icon != null ) {
68
      menuItem.setGraphic(
69
          FontAwesomeIconFactory.get().createIcon( action.icon ) );
70
    }
71
72
    menuItem.setOnAction( action.action );
73
74
    if( action.disable != null ) {
75
      menuItem.disableProperty().bind( action.disable );
76
    }
77
78
    menuItem.setMnemonicParsing( true );
79
80
    return menuItem;
81
  }
82
83
  public static ToolBar createToolBar( final Action... actions ) {
84
    return new ToolBar( createToolBarButtons( actions ) );
85
  }
86
87
  public static Node[] createToolBarButtons( final Action... actions ) {
88
    Node[] buttons = new Node[ actions.length ];
89
    for( int i = 0; i < actions.length; i++ ) {
90
      buttons[ i ] = (actions[ i ] != null)
91
          ? createToolBarButton( actions[ i ] )
92
          : new Separator();
93
    }
94
    return buttons;
95
  }
96
97
  public static Button createToolBarButton( final Action action ) {
98
    final Button button = new Button();
99
    button.setGraphic(
100
        FontAwesomeIconFactory
101
            .get()
102
            .createIcon( action.icon, "1.2em" ) );
103
104
    String tooltip = action.text;
105
106
    if( tooltip.endsWith( "..." ) ) {
107
      tooltip = tooltip.substring( 0, tooltip.length() - 3 );
108
    }
109
110
    if( action.accelerator != null ) {
111
      tooltip += " (" + action.accelerator.getDisplayText() + ')';
112
    }
113
114
    button.setTooltip( new Tooltip( tooltip ) );
115
    button.setFocusTraversable( false );
116
    button.setOnAction( action.action );
117
118
    if( action.disable != null ) {
119
      button.disableProperty().bind( action.disable );
120
    }
121
122
    return button;
123
  }
124
}
1125
A src/main/java/com/scrivenvar/util/ProtocolResolver.java
1
package com.scrivenvar.util;
2
3
import java.io.File;
4
import java.net.URI;
5
import java.net.URL;
6
7
import static com.scrivenvar.Constants.DEFINITION_PROTOCOL_UNKNOWN;
8
9
/**
10
 * Responsible for determining the protocol of a resource.
11
 */
12
public class ProtocolResolver {
13
  /**
14
   * Returns the protocol for a given URI or filename.
15
   *
16
   * @param resource Determine the protocol for this URI or filename.
17
   * @return The protocol for the given source.
18
   */
19
  public static String getProtocol( final String resource ) {
20
    String protocol;
21
22
    try {
23
      final URI uri = new URI( resource );
24
25
      if( uri.isAbsolute() ) {
26
        protocol = uri.getScheme();
27
      }
28
      else {
29
        final URL url = new URL( resource );
30
        protocol = url.getProtocol();
31
      }
32
    } catch( final Exception e ) {
33
      // Could be HTTP, HTTPS?
34
      if( resource.startsWith( "//" ) ) {
35
        throw new IllegalArgumentException( "Relative context: " + resource );
36
      }
37
      else {
38
        final File file = new File( resource );
39
        protocol = getProtocol( file );
40
      }
41
    }
42
43
    return protocol;
44
  }
45
46
  /**
47
   * Returns the protocol for a given file.
48
   *
49
   * @param file Determine the protocol for this file.
50
   * @return The protocol for the given file.
51
   */
52
  public static String getProtocol( final File file ) {
53
    String result;
54
55
    try {
56
      result = file.toURI().toURL().getProtocol();
57
    } catch( final Exception e ) {
58
      result = DEFINITION_PROTOCOL_UNKNOWN;
59
    }
60
61
    return result;
62
  }
63
}
164
A src/main/java/com/scrivenvar/util/ResourceWalker.java
1
package com.scrivenvar.util;
2
3
import java.io.IOException;
4
import java.net.URISyntaxException;
5
import java.nio.file.*;
6
import java.util.function.Consumer;
7
8
import static java.nio.file.FileSystems.newFileSystem;
9
import static java.util.Collections.emptyMap;
10
11
/**
12
 * Responsible for finding file resources.
13
 */
14
public class ResourceWalker {
15
  private static final PathMatcher PATH_MATCHER =
16
      FileSystems.getDefault().getPathMatcher( "glob:**.ttf" );
17
18
  /**
19
   * @param dirName The root directory to scan for files matching the glob.
20
   * @param c       The consumer function to call for each matching path found.
21
   * @throws URISyntaxException Could not convert the resource to a URI.
22
   * @throws IOException        Could not walk the tree.
23
   */
24
  public static void walk( final String dirName, final Consumer<Path> c )
25
      throws URISyntaxException, IOException {
26
    final var resource = ResourceWalker.class.getResource( dirName );
27
28
    if( resource != null ) {
29
      final var uri = resource.toURI();
30
      final var path = uri.getScheme().equals( "jar" )
31
          ? newFileSystem( uri, emptyMap() ).getPath( dirName )
32
          : Paths.get( uri );
33
      final var walk = Files.walk( path, 10 );
34
35
      for( final var it = walk.iterator(); it.hasNext(); ) {
36
        final Path p = it.next();
37
        if( PATH_MATCHER.matches( p ) ) {
38
          c.accept( p );
39
        }
40
      }
41
    }
42
  }
43
}
144
A src/main/java/com/scrivenvar/util/StageState.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
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
31
import javafx.application.Platform;
32
import javafx.scene.shape.Rectangle;
33
import javafx.stage.Stage;
34
import javafx.stage.WindowEvent;
35
36
/**
37
 * Saves and restores Stage state (window bounds, maximized, fullScreen).
38
 */
39
public class StageState {
40
41
  public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition";
42
  public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor";
43
  public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview";
44
45
  private final Stage mStage;
46
  private final Preferences mState;
47
48
  private Rectangle normalBounds;
49
  private boolean runLaterPending;
50
51
  public StageState( final Stage stage, final Preferences state ) {
52
    mStage = stage;
53
    mState = state;
54
55
    restore();
56
57
    stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() );
58
59
    stage.xProperty().addListener( ( ob, o, n ) -> boundsChanged() );
60
    stage.yProperty().addListener( ( ob, o, n ) -> boundsChanged() );
61
    stage.widthProperty().addListener( ( ob, o, n ) -> boundsChanged() );
62
    stage.heightProperty().addListener( ( ob, o, n ) -> boundsChanged() );
63
  }
64
65
  private void save() {
66
    final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds;
67
68
    if( bounds != null ) {
69
      mState.putDouble( "windowX", bounds.getX() );
70
      mState.putDouble( "windowY", bounds.getY() );
71
      mState.putDouble( "windowWidth", bounds.getWidth() );
72
      mState.putDouble( "windowHeight", bounds.getHeight() );
73
    }
74
75
    mState.putBoolean( "windowMaximized", mStage.isMaximized() );
76
    mState.putBoolean( "windowFullScreen", mStage.isFullScreen() );
77
  }
78
79
  private void restore() {
80
    final double x = mState.getDouble( "windowX", Double.NaN );
81
    final double y = mState.getDouble( "windowY", Double.NaN );
82
    final double w = mState.getDouble( "windowWidth", Double.NaN );
83
    final double h = mState.getDouble( "windowHeight", Double.NaN );
84
    final boolean maximized = mState.getBoolean( "windowMaximized", false );
85
    final boolean fullScreen = mState.getBoolean( "windowFullScreen", false );
86
87
    if( !Double.isNaN( x ) && !Double.isNaN( y ) ) {
88
      mStage.setX( x );
89
      mStage.setY( y );
90
    } // else: default behavior is center on screen
91
92
    if( !Double.isNaN( w ) && !Double.isNaN( h ) ) {
93
      mStage.setWidth( w );
94
      mStage.setHeight( h );
95
    } // else: default behavior is use scene size
96
97
    if( fullScreen != mStage.isFullScreen() ) {
98
      mStage.setFullScreen( fullScreen );
99
    }
100
101
    if( maximized != mStage.isMaximized() ) {
102
      mStage.setMaximized( maximized );
103
    }
104
  }
105
106
  /**
107
   * Remembers the window bounds when the window is not iconified, maximized or
108
   * in fullScreen.
109
   */
110
  private void boundsChanged() {
111
    // avoid too many (and useless) runLater() invocations
112
    if( runLaterPending ) {
113
      return;
114
    }
115
116
    runLaterPending = true;
117
118
    // must use runLater() to ensure that change of all properties
119
    // (x, y, width, height, iconified, maximized and fullScreen)
120
    // has finished
121
    Platform.runLater( () -> {
122
      runLaterPending = false;
123
124
      if( isNormalState() ) {
125
        normalBounds = getStageBounds();
126
      }
127
    } );
128
  }
129
130
  private boolean isNormalState() {
131
    return !mStage.isIconified() &&
132
        !mStage.isMaximized() &&
133
        !mStage.isFullScreen();
134
  }
135
136
  private Rectangle getStageBounds() {
137
    return new Rectangle(
138
        mStage.getX(),
139
        mStage.getY(),
140
        mStage.getWidth(),
141
        mStage.getHeight()
142
    );
143
  }
144
}
1145
A src/main/java/com/scrivenvar/util/Utils.java
1
/*
2
 * Copyright 2020 Karl Tauber and White Magic Software, Ltd.
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
 * Responsible for trimming, storing, and retrieving strings.
34
 */
35
public class Utils {
36
37
  public static String ltrim( final String s ) {
38
    int i = 0;
39
40
    while( i < s.length() && Character.isWhitespace( s.charAt( i ) ) ) {
41
      i++;
42
    }
43
44
    return s.substring( i );
45
  }
46
47
  public static String rtrim( final String s ) {
48
    int i = s.length() - 1;
49
50
    while( i >= 0 && Character.isWhitespace( s.charAt( i ) ) ) {
51
      i--;
52
    }
53
54
    return s.substring( 0, i + 1 );
55
  }
56
57
  public static String[] getPrefsStrings( final Preferences prefs,
58
                                          String key ) {
59
    final ArrayList<String> arr = new ArrayList<>( 256 );
60
61
    for( int i = 0; i < 10000; i++ ) {
62
      final String s = prefs.get( key + (i + 1), null );
63
64
      if( s == null ) {
65
        break;
66
      }
67
68
      arr.add( s );
69
    }
70
71
    return arr.toArray( new String[ 0 ] );
72
  }
73
74
  public static void putPrefsStrings( Preferences prefs, String key,
75
                                      String[] strings ) {
76
    for( int i = 0; i < strings.length; i++ ) {
77
      prefs.put( key + (i + 1), strings[ i ] );
78
    }
79
80
    for( int i = strings.length; prefs.get( key + (i + 1),
81
                                            null ) != null; i++ ) {
82
      prefs.remove( key + (i + 1) );
83
    }
84
  }
85
}
186
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/.gitignore
1
app.properties
12
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 2020 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
29
.markdown-editor {
30
  -fx-font-size: 11pt;
31
}
32
33
/* Subtly highlight the current paragraph. */
34
.markdown-editor .paragraph-box:has-caret {
35
  -fx-background-color: #fcfeff;
36
}
37
38
/* Light colour for selection highlight. */
39
.markdown-editor .selection {
40
  -fx-fill: #a6d2ff;
41
}
42
43
/* Decoration for words not found in the lexicon. */
44
.markdown-editor .spelling {
45
  -rtfx-underline-color: rgba(255, 131, 67, .7);
46
  -rtfx-underline-dash-array: 4, 2;
47
  -rtfx-underline-width: 2;
48
  -rtfx-underline-cap: round;
49
}
150
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
# Main Application Window
3
# ########################################################################
4
5
# The application title should exist only once in the entire code base.
6
# All other references should either refer to this value via the Messages
7
# class, or indirectly using ${Main.title}.
8
Main.title=Scrivenvar
9
10
Main.menu.file=_File
11
Main.menu.file.new=_New
12
Main.menu.file.open=_Open...
13
Main.menu.file.close=_Close
14
Main.menu.file.close_all=Close All
15
Main.menu.file.save=_Save
16
Main.menu.file.save_as=Save _As
17
Main.menu.file.save_all=Save A_ll
18
Main.menu.file.exit=E_xit
19
20
Main.menu.edit=_Edit
21
Main.menu.edit.copy.html=Copy _HTML
22
Main.menu.edit.undo=_Undo
23
Main.menu.edit.redo=_Redo
24
Main.menu.edit.cut=Cu_t
25
Main.menu.edit.copy=_Copy
26
Main.menu.edit.paste=_Paste
27
Main.menu.edit.find=_Find
28
Main.menu.edit.find.next=Find _Next
29
Main.menu.edit.preferences=_Preferences
30
31
Main.menu.insert=_Insert
32
Main.menu.insert.bold=Bold
33
Main.menu.insert.italic=Italic
34
Main.menu.insert.superscript=Superscript
35
Main.menu.insert.subscript=Subscript
36
Main.menu.insert.strikethrough=Strikethrough
37
Main.menu.insert.blockquote=Blockquote
38
Main.menu.insert.code=Inline Code
39
Main.menu.insert.fenced_code_block=Fenced Code Block
40
Main.menu.insert.fenced_code_block.prompt=Enter code here
41
Main.menu.insert.link=Link...
42
Main.menu.insert.image=Image...
43
Main.menu.insert.heading.1=Heading 1
44
Main.menu.insert.heading.1.prompt=heading 1
45
Main.menu.insert.heading.2=Heading 2
46
Main.menu.insert.heading.2.prompt=heading 2
47
Main.menu.insert.heading.3=Heading 3
48
Main.menu.insert.heading.3.prompt=heading 3
49
Main.menu.insert.unordered_list=Unordered List
50
Main.menu.insert.ordered_list=Ordered List
51
Main.menu.insert.horizontal_rule=Horizontal Rule
52
53
Main.menu.help=_Help
54
Main.menu.help.about=About ${Main.title}
55
56
# ########################################################################
57
# Status Bar
58
# ########################################################################
59
60
Main.statusbar.text.offset=offset
61
Main.statusbar.line=Line {0} of {1}, ${Main.statusbar.text.offset} {2}
62
Main.statusbar.state.default=OK
63
Main.statusbar.parse.error={0} (near ${Main.statusbar.text.offset} {1})
64
65
# ########################################################################
66
# Preferences
67
# ########################################################################
68
69
Preferences.r=R
70
Preferences.r.script=Startup Script
71
Preferences.r.script.desc=Script runs prior to executing R statements within the document.
72
Preferences.r.directory=Working Directory
73
Preferences.r.directory.desc=Value assigned to $application.r.working.directory$ and usable in the startup script.
74
Preferences.r.delimiter.began=Delimiter Prefix
75
Preferences.r.delimiter.began.desc=Prefix of expression that wraps inserted definitions.
76
Preferences.r.delimiter.ended=Delimiter Suffix
77
Preferences.r.delimiter.ended.desc=Suffix of expression that wraps inserted definitions.
78
79
Preferences.images=Images
80
Preferences.images.directory=Relative Directory
81
Preferences.images.directory.desc=Path prepended to embedded images referenced using local file paths.
82
Preferences.images.suffixes=Extensions
83
Preferences.images.suffixes.desc=Preferred order of image file types to embed, separated by spaces.
84
85
Preferences.definitions=Definitions
86
Preferences.definitions.path=File name
87
Preferences.definitions.path.desc=Absolute path to interpolated string definitions.
88
89
# ########################################################################
90
# Definition Pane and its Tree View
91
# ########################################################################
92
93
Definition.menu.create=Create
94
Definition.menu.rename=Rename
95
Definition.menu.remove=Delete
96
Definition.menu.add.default=Undefined
97
98
# ########################################################################
99
# Failure messages with respect to YAML files.
100
# ########################################################################
101
yaml.error.open=Could not open YAML file (ensure non-empty file).
102
yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
103
yaml.error.missing=Empty definition value for key ''{0}''.
104
yaml.error.tree.form=Unassigned definition near ''{0}''.
105
106
# ########################################################################
107
# File Editor
108
# ########################################################################
109
110
FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1}
111
FileEditor.loadFailed.title=Load
112
FileEditor.loadFailed.reason.permissions=File must be readable and writable.
113
FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
114
FileEditor.saveFailed.title=Save
115
116
# ########################################################################
117
# File Open
118
# ########################################################################
119
120
Dialog.file.choose.open.title=Open File
121
Dialog.file.choose.save.title=Save File
122
123
Dialog.file.choose.filter.title.source=Source Files
124
Dialog.file.choose.filter.title.definition=Definition Files
125
Dialog.file.choose.filter.title.xml=XML Files
126
Dialog.file.choose.filter.title.all=All Files
127
128
# ########################################################################
129
# Alert Dialog
130
# ########################################################################
131
132
Alert.file.close.title=Close
133
Alert.file.close.text=Save changes to {0}?
134
135
# ########################################################################
136
# Definition Pane
137
# ########################################################################
138
139
Pane.definition.node.root.title=Definitions
140
Pane.definition.button.create.label=_Create
141
Pane.definition.button.rename.label=_Rename
142
Pane.definition.button.delete.label=_Delete
143
Pane.definition.button.create.tooltip=Add new item (Insert)
144
Pane.definition.button.rename.tooltip=Rename selected item (F2)
145
Pane.definition.button.delete.tooltip=Delete selected items (Delete)
146
147
# Controls ###############################################################
148
149
# ########################################################################
150
# Browse File
151
# ########################################################################
152
153
BrowseFileButton.chooser.title=Browse for local file
154
BrowseFileButton.chooser.allFilesFilter=All Files
155
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
156
157
# Dialogs ################################################################
158
159
# ########################################################################
160
# Image
161
# ########################################################################
162
163
Dialog.image.title=Image
164
Dialog.image.chooser.imagesFilter=Images
165
Dialog.image.previewLabel.text=Markdown Preview\:
166
Dialog.image.textLabel.text=Alternate Text\:
167
Dialog.image.titleLabel.text=Title (tooltip)\:
168
Dialog.image.urlLabel.text=Image URL\:
169
170
# ########################################################################
171
# Hyperlink
172
# ########################################################################
173
174
Dialog.link.title=Link
175
Dialog.link.previewLabel.text=Markdown Preview\:
176
Dialog.link.textLabel.text=Link Text\:
177
Dialog.link.titleLabel.text=Title (tooltip)\:
178
Dialog.link.urlLabel.text=Link URL\:
179
180
# ########################################################################
181
# About
182
# ########################################################################
183
184
Dialog.about.title=About
185
Dialog.about.header=${Main.title}
186
Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
1187
A src/main/resources/com/scrivenvar/preview/webview.css
1
/* RESET ***/
2
html{box-sizing:border-box;font-size:12pt}body,h1,h2,h3,h4,h5,h6,ol,p,ul{margin:0;padding:0}img{max-width:100%;height:auto}table{table-collapse:collapse;table-spacing:0;border-spacing:0}
3
4
/* BODY ***/
5
body {
6
  /* Must be bundled in JAR file. */
7
  font-family: "Vollkorn", serif;
8
  background-color: #fff;
9
  margin: 0 auto;
10
  max-width: 960px;
11
  line-height: 1.6;
12
  color: #454545;
13
  padding: 0 1em;
14
}
15
16
body>*:first-child {
17
  margin-top: 0 !important;
18
}
19
20
body>*:last-child {
21
  margin-bottom: 0 !important;
22
}
23
24
/* BLOCKS ***/
25
p, blockquote, ul, ol, dl, table, pre {
26
  margin: 1em 0;
27
}
28
29
/* HEADINGS ***/
30
h1, h2, h3, h4, h5, h6 {
31
  font-weight: bold;
32
  margin: 1em 0 .5em;
33
}
34
35
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
36
h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
37
  font-size: inherit;
38
}
39
40
h1 {
41
  font-size: 21pt;
42
}
43
44
h2 {
45
  font-size: 18pt;
46
  border-bottom: 1px solid #ccc;
47
}
48
49
h3 {
50
  font-size: 15pt;
51
}
52
53
h4 {
54
  font-size: 13.5pt;
55
}
56
57
h5 {
58
  font-size: 12pt;
59
}
60
61
h6 {
62
  font-size: 10.5pt;
63
}
64
65
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
66
  margin-top: .5em;
67
}
68
69
/* LINKS ***/
70
a {
71
  color: #0077aa;
72
  text-decoration: none;
73
}
74
75
a:hover {
76
  text-decoration: underline;
77
}
78
79
/* BULLET LISTS ***/
80
ul, ol {
81
  display: block;
82
  list-style: disc outside none;
83
  margin: 1em 0;
84
  padding: 0 0 0 2em;
85
}
86
87
ol {
88
  list-style-type: decimal;
89
}
90
91
ul ul, ol ul,
92
ol ol, ul ol {
93
  list-style-position: inside;
94
  margin-left: 1em;
95
}
96
97
ul ul, ol ul {
98
  list-style-type: circle;
99
}
100
101
ol ol, ul ol {
102
  list-style-type: lower-latin;
103
}
104
105
/* DEFINITION LISTS ***/
106
dl {
107
  /** Horizontal scroll bar will appear if set to 100%. */
108
  width: 99%;
109
  overflow: hidden;
110
  padding-left: 1em;
111
}
112
113
dl dt {
114
  font-weight: bold;
115
  float: left;
116
  width: 20%;
117
  clear: both;
118
  position: relative;
119
}
120
121
dl dd {
122
  float: right;
123
  width: 79%;
124
  padding-bottom: .5em;
125
  margin-left: 0;
126
}
127
128
/* CODE ***/
129
pre, code, tt {
130
  /* Must be bundled in JAR file. */
131
  font-family: "Fira Code", monospace;
132
  font-size: 10pt;
133
  background-color: #f8f8f8;
134
  text-decoration: none;
135
  white-space: pre-wrap;
136
  word-wrap: break-word;
137
  overflow-wrap: anywhere;
138
  border-radius: .125em;
139
}
140
141
code, tt {
142
  padding: .25em;
143
}
144
145
pre > code {
146
  /* Reset the padding. */
147
  padding: 0;
148
  border: none;
149
  background: transparent;
150
}
151
152
pre {
153
  border: .125em solid #ccc;
154
  overflow: auto;
155
  /* Assign the new padding, independently from previous. */
156
  padding: .25em .5em;
157
}
158
159
pre code, pre tt {
160
  background-color: transparent;
161
  border: none;
162
}
163
164
/* QUOTES ***/
165
blockquote {
166
  border-left: .25em solid #ccc;
167
  padding: 0 1em;
168
  color: #777;
169
}
170
171
blockquote>:first-child {
172
  margin-top: 0;
173
}
174
175
blockquote>:last-child {
176
  margin-bottom: 0;
177
}
178
179
/* HORIZONTAL RULES ***/
180
hr {
181
  clear: both;
182
  margin: 1.5em 0 1.5em;
183
  height: 0;
184
  overflow: hidden;
185
  border: none;
186
  background: transparent;
187
  border-bottom: .125em solid #ccc;
188
}
189
190
/* TABLES ***/
191
table {
192
  width: 100%;
193
}
194
195
tr:nth-child(odd) {
196
  background-color: #eee;
197
}
198
199
th {
200
  background-color: #454545;
201
  color: #fff;
202
}
203
204
th, td {
205
  text-align: left;
206
  padding: 0 1em;
207
}
208
209
/* IMAGES ***/
210
img {
211
  max-width: 100%;
212
}
1213
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
# Application
3
# ########################################################################
4
5
application.title=scrivenvar
6
application.package=com/${application.title}
7
application.messages= com.${application.title}.messages
8
9
# Suppress multiple file modified notifications for one logical modification.
10
# Given in milliseconds.
11
application.watchdog.timeout=50
12
13
# ########################################################################
14
# Preferences
15
# ########################################################################
16
17
preferences.root=com.${application.title}
18
preferences.root.state=state
19
preferences.root.options=options
20
preferences.root.definition.source=definition.source
21
22
# ########################################################################
23
# File and Path References
24
# ########################################################################
25
file.stylesheet.scene=${application.package}/scene.css
26
file.stylesheet.markdown=${application.package}/editor/markdown.css
27
file.stylesheet.preview=webview.css
28
file.stylesheet.xml=${application.package}/xml.css
29
30
file.logo.16 =${application.package}/logo16.png
31
file.logo.32 =${application.package}/logo32.png
32
file.logo.128=${application.package}/logo128.png
33
file.logo.256=${application.package}/logo256.png
34
file.logo.512=${application.package}/logo512.png
35
36
# Default file name when a new file is created.
37
# This ensures that the file type can always be
38
# discerned so that the correct type of variable
39
# reference can be inserted.
40
file.default=untitled.md
41
file.definition.default=variables.yaml
42
43
# ########################################################################
44
# File name Extensions
45
# ########################################################################
46
47
# Comma-separated list of definition file name extensions.
48
definition.file.ext.json=*.json
49
definition.file.ext.toml=*.toml
50
definition.file.ext.yaml=*.yml,*.yaml
51
definition.file.ext.properties=*.properties,*.props
52
53
# Comma-separated list of file name extensions.
54
file.ext.rmarkdown=*.Rmd
55
file.ext.rxml=*.Rxml
56
file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml}
57
file.ext.definition=${definition.file.ext.yaml}
58
file.ext.xml=*.xml,${file.ext.rxml}
59
file.ext.all=*.*
60
61
# File name extension search order for images.
62
file.ext.image.order=svg pdf png jpg tiff
63
64
# ########################################################################
65
# Variable Name Editor
66
# ########################################################################
67
68
# Maximum number of characters for a variable name. A variable is defined
69
# as one or more non-whitespace characters up to this maximum length.
70
editor.variable.maxLength=256
71
72
# ########################################################################
73
# Dialog Preferences
74
# ########################################################################
75
76
dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
77
dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
78
dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
79
80
# Ensures a consistent button order for alert dialogs across platforms (because
81
# the default button order on Linux defies all logic).
82
dialog.alert.button.order=${dialog.alert.button.order.windows}
183
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
}
A src/main/resources/fonts/firacode/FiraCode-Bold.ttf
Binary file
A src/main/resources/fonts/firacode/FiraCode-Light.ttf
Binary file
A src/main/resources/fonts/firacode/FiraCode-Medium.ttf
Binary file
A src/main/resources/fonts/firacode/FiraCode-Regular.ttf
Binary file
A src/main/resources/fonts/firacode/FiraCode-Retina.ttf
Binary file
A src/main/resources/fonts/firacode/FiraCode-SemiBold.ttf
Binary file
A src/main/resources/fonts/vollkorn/Vollkorn-Bold.ttf
Binary file
A src/main/resources/fonts/vollkorn/Vollkorn-BoldItalic.ttf
Binary file
A src/main/resources/fonts/vollkorn/Vollkorn-Italic.ttf
Binary file
A src/main/resources/fonts/vollkorn/Vollkorn-Regular.ttf
Binary file
A src/main/resources/fonts/vollkorn/VollkornSC-Bold.ttf
Binary file
A src/main/resources/fonts/vollkorn/VollkornSC-Regular.ttf
Binary file
A src/main/resources/lexicons/README.md
1
# Building
2
3
The lexicon files are retrieved from SymSpell in the parent directory:
4
5
svn export \
6
  https://github.com/wolfgarbe/SymSpell/trunk/SymSpell.FrequencyDictionary/ lexicons
7
8
The lexicons and bigrams are both space-separated, but parsing a
9
tab-delimited file is easier, so change them to tab-separated files.
110
A src/main/resources/lexicons/de.txt
Binary file
A src/main/resources/lexicons/en.txt
Binary file
A src/main/resources/lexicons/es.txt
Binary file
A src/main/resources/lexicons/ext/README.md
1
# Overview
2
3
Lexicons in this directory are meant to relate to a particular subject
4
(medicine, chemistry, math, sports, and such), extend the main lexicon,
5
or not be in common use.
6
17
A src/main/resources/lexicons/ext/contractions.txt
11
2
'aight
3
ain't
4
amn't
5
aren't
6
can't
7
'cause
8
couldn't
9
couldn't've
10
could've
11
daren't
12
daresn't
13
dasn't
14
didn't
15
doesn't
16
don't
17
dunno
18
d'ye
19
e'er
20
everybody's
21
everyone's
22
g'day
23
gimme
24
giv'n
25
gonna
26
gon't
27
gotta
28
hadn't
29
had've
30
hasn't
31
haven't
32
he'd
33
he'll
34
he's
35
he've
36
how'd
37
howdy
38
how'll
39
how're
40
how's
41
how've
42
i'd
43
i'dn't've
44
i'd've
45
i'll
46
i'm
47
i'm'a
48
imma
49
innit
50
isn't
51
it'd
52
it'll
53
it's
54
i've
55
let's
56
ma'am
57
mayn't
58
may've
59
methinks
60
mightn't
61
might've
62
mustn't
63
mustn't've
64
must've
65
needn't
66
ne'er
67
o'clock
68
o'er
69
ol'
70
oughtn't
71
shalln't
72
shan't
73
she'd
74
she'll
75
she's
76
shouldn't
77
shouldn't've
78
should've
79
somebody's
80
someone's
81
something's
82
so're
83
that'd
84
that'll
85
that're
86
that's
87
there'd
88
there'll
89
there're
90
there's
91
these'd
92
these'll
93
these're
94
these've
95
they'd
96
they'll
97
they're
98
they've
99
this's
100
those're
101
those've
102
'tis
103
to've
104
'twas
105
'twouldn't
106
wanna
107
wasn't
108
we'd
109
we'd've
110
we'll
111
we're
112
weren't
113
we've
114
what'd
115
what'll
116
what're
117
what's
118
what've
119
when'd
120
when'll
121
when's
122
where'd
123
where'll
124
where're
125
where's
126
where've
127
which'd
128
which'll
129
which're
130
which's
131
which've
132
who'd
133
who'd've
134
who'll
135
who're
136
who's
137
who've
138
why'd
139
why'll
140
why're
141
why's
142
willn't
143
won't
144
wouldn't
145
wouldn't've
146
would've
147
y'all
148
y'all'd've
149
y'all're
150
you'd
151
you'dn't've
152
you'll
153
you're
154
you've
155
A src/main/resources/lexicons/ext/tech.txt
1
analytics	130337
2
hotspot	130022
3
instantiation	130000
4
onboarding	129953
5
biometric	129795
6
anamorphic	129777
7
benchmarking	129772
8
cybersecurity	129769
9
barcode	129757
10
splitter	129755
11
keychain	129719
12
crowdfunding	129696
13
polymorphism	129688
14
automata	129666
15
shockwave	129658
16
profiler	129648
17
kerning	129646
18
nanometer	129630
19
meridiem	129624
20
influencer	129618
21
passcode	129617
22
sexting	129607
23
cryptology	129606
24
biometrics	129606
25
bitcoin	129599
26
specular	129598
27
accelerometer	129588
28
googolplex	129583
29
grayscale	129576
30
ascender	129571
31
pixelated	129569
32
rockstar	129565
33
ragdoll	129564
34
cyberattack	129564
35
cryptanalysis	129562
36
ransomware	129553
37
crowdsourcing	129552
38
hackathon	129551
39
audiobook	129544
40
degauss	129543
41
attenuator	129540
42
jetpack	129538
43
packrat	129536
44
backlight	129535
45
bootable	129530
46
octothorpe	129529
47
newsfeed	129525
48
extranet	129523
49
failover	129516
50
cyberbullying	129516
51
neumann	129515
52
capacitive	129514
53
backlit	129511
54
millimicron	129507
55
inductor	129505
56
workgroup	129502
57
journaling	129500
58
middleware	129499
59
spooler	129497
60
clamshell	129495
61
wireframe	129494
62
modularity	129493
63
strikethrough	129489
64
petabyte	129487
65
jughead	129482
66
acyclic	129482
67
gearhead	129478
68
stateful	129473
69
submenu	129467
70
pseudorandom	129463
71
earbuds	129461
72
narrowband	129460
73
recordable	129457
74
unallocated	129455
75
mappable	129455
76
chipset	129454
77
multicast	129447
78
loopback	129444
79
pixelate	129441
80
cryptographic	129441
81
pixelation	129438
82
autocorrect	129438
83
teraflop	129437
84
digitizer	129436
85
tunnelling	129434
86
deduplication	129434
87
subwoofer	129433
88
touchpad	129429
89
namespace	129428
90
microcontroller	129428
91
geolocation	129428
92
telepresence	129427
93
driverless	129426
94
photolithography	129425
95
multiphase	129425
96
verifier	129424
97
robocall	129424
98
autofocus	129424
99
kilobit	129422
100
hacktivist	129419
101
geocache	129415
102
rasterize	129412
103
plaintext	129411
104
pipelining	129411
105
technobabble	129409
106
defragment	129409
107
connectionless	129409
108
homomorphic	129407
109
demodulator	129406
110
datagram	129406
111
activex	129406
112
normalisation	129404
113
blackhole	129402
114
cyberstalker	129401
115
multifunction	129400
116
undirected	129397
117
ciphertext	129397
118
superspeed	129396
119
spacebar	129395
120
cyberwar	129395
121
borderless	129395
122
transcode	129393
123
cyberbully	129393
124
multimeter	129392
125
dropship	129391
126
yottabyte	129390
127
infector	129390
128
superclass	129389
129
tooltip	129388
130
dereference	129387
131
combinator	129386
132
milliwatt	129385
133
cyberstalking	129384
134
subfolder	129383
135
wideband	129382
136
noncontiguous	129382
137
ferroelectric	129382
138
cybersquatting	129378
139
autofill	129378
140
trackpad	129376
141
associatively	129376
142
luggable	129374
143
seamonkey	129373
144
defragmentation	129373
145
starcraft	129371
146
obliquing	129371
147
leadless	129371
148
greeking	129371
149
upgradeable	129370
150
radiosity	129370
151
transcoding	129369
152
quintillionth	129369
153
bitmapped	129369
154
subdirectory	129368
155
degausser	129368
156
curtiss	129368
157
scunthorpe	129367
158
undelete	129365
159
gigaflops	129365
160
darknet	129365
161
zettabyte	129364
162
topologies	129363
163
spidering	129363
164
photorealism	129363
165
multithreading	129363
166
deallocate	129363
167
mersenne	129362
168
machinima	129361
169
satisfiable	129360
170
laserjet	129360
171
multicore	129359
172
microblog	129359
173
megaflops	129359
174
homeomorphic	129359
175
microblogging	129358
176
kilobaud	129358
177
cyberwarfare	129358
178
microarchitecture	129357
179
autosave	129357
180
wirelessly	129356
181
sneakernet	129355
182
textbox	129354
183
obfuscator	129354
184
microkernel	129353
185
substring	129352
186
macroinstruction	129352
187
endianness	129352
188
indexable	129351
189
backtick	129351
190
unshielded	129350
191
cleartext	129350
192
autocomplete	129349
193
abandonware	129349
194
hacktivism	129348
195
antikythera	129348
196
stereolithography	129347
197
photorealistic	129347
198
macrovision	129347
199
greasemonkey	129347
200
geotagging	129347
201
disassembler	129346
202
spacewar	129345
203
pluggable	129345
204
kilobits	129345
205
webcomic	129344
206
unfollow	129344
207
photosensor	129344
208
petaflop	129344
209
garageband	129344
210
truetype	129343
211
subnetwork	129342
212
backpropagation	129342
213
supercomputing	129340
214
smartwatch	129340
215
unbundled	129339
216
smilies	129339
217
milliamp	129339
218
bytecode	129339
219
trackpoint	129337
220
slipstreaming	129337
221
monospace	129337
222
memoization	129337
223
scaleable	129336
224
respawn	129335
225
multicasting	129335
226
geocacher	129335
227
workgroups	129334
228
ferrofluid	129334
229
smartdrive	129333
230
subsampling	129332
231
rasterization	129332
232
guiltware	129332
233
defragger	129332
234
satisfiability	129331
235
activision	129331
236
subdirectories	129330
237
segfault	129330
238
flamebait	129330
239
framebuffer	129329
240
defragging	129329
241
decompiler	129329
242
unshift	129328
243
memristor	129328
244
zebibyte	129327
245
semiprime	129327
246
rotoscoping	129327
247
hypertransport	129327
248
smartmedia	129326
249
grayware	129326
250
defragmenting	129326
251
defragmenter	129326
252
repagination	129325
253
subnetting	129324
254
skeuomorphism	129324
255
screencast	129324
256
stylesheet	129323
257
superintelligence	129322
258
multitenancy	129322
259
datastore	129322
260
autoplay	129322
261
repaginate	129321
262
macbook	129321
263
geotagged	129321
264
baudrate	129321
265
transmeta	129320
266
screwless	129320
267
nameserver	129320
268
interexchange	129320
269
geocoding	129319
270
downloader	129319
271
autodiscovery	129319
272
extortion	65752
273
emoji	65684
274
googol	65618
275
backside	65388
276
fibre	65387
277
metre	65333
278
royale	65173
279
radix	65093
280
hotdog	65091
281
lecher	65062
282
uptime	65009
283
unbound	64979
284
eniac	64975
285
synaptic	64966
286
voxel	64926
287
selfie	64917
288
uplink	64887
289
fanboy	64857
290
defrag	64849
291
nondisclosure	64839
292
qubit	64828
293
yippie	64821
294
gearhead	64819
295
subnet	64818
296
endian	64798
297
bezier	64797
298
reallocation	64796
299
telephonic	64789
300
mosfet	64777
301
mutex	64775
302
inkjet	64772
303
gobbing	64768
304
shader	64766
305
ultralight	64755
306
hackers	64746
307
pacman	64742
308
unlink	64741
309
undock	64740
310
understroke	64738
311
beginners	64736
312
photoscope	64731
313
gantt	64725
314
programmers	64722
315
todays	64720
316
moores	64716
317
fullscreen	64715
318
moveless	64708
319
reformatted	64704
320
deallocate	64704
321
laserdisc	64702
322
macos	64700
323
nonactive	64697
324
nonadjacent	64696
325
hotfix	64695
326
keylogger	64694
327
geotag	64691
328
oreilly	64681
329
exabit	64678
330
jailbroken	64677
331
fuzzer	64676
332
noninteractive	64673
333
multifactor	64672
334
letterspacing	64671
335
preinstall	64669
336
multiboot	64666
337
runescape	64665
338
micropayment	64664
339
numpad	64663
340
preinstalled	64661
341
jailbreaking	64660
342
attend	2158
343
withstand	1809
344
transpire	1116
345
reading	1110
346
texture	1065
347
capitalize	832
348
calling	779
349
unfold	767
350
starboard	679
351
commode	625
352
doing	594
353
textbook	499
354
unease	378
355
unpack	358
356
keycard	231
357
mainspring	207
358
grr	180
359
geocaching	167
360
microbus	160
361
mp3	147
362
svg	139
363
shifted	128
364
texted	127
365
towheaded	118
366
mineshaft	115
367
nonparty	95
368
crossbite	80
369
resignedness	69
370
msrp	61
371
inbreak	53
372
nanocomposite	44
373
md5	44
374
neomorphic	41
375
superstrain	28
376
lifers	27
377
multination	26
378
smartwatch	22
379
antilibration	22
380
zapf	20
381
mp4	20
1382
A src/main/resources/lexicons/fr.txt
Binary file
A src/main/resources/lexicons/he.txt
Binary file
A src/main/resources/lexicons/it.txt
Binary file
A src/main/resources/lexicons/ru.txt
Binary file
A src/main/resources/lexicons/zh.txt
Binary file
A src/test/java/com/scrivenvar/definition/TreeItemInterpolatorTest.java
1
/*
2
 * Copyright 2020 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.TreeItem;
31
import org.junit.jupiter.api.Test;
32
33
import static java.lang.String.format;
34
import static org.junit.jupiter.api.Assertions.assertEquals;
35
36
public class TreeItemInterpolatorTest {
37
38
  private final static String AUTHOR_FIRST = "FirstName";
39
  private final static String AUTHOR_LAST = "LastName";
40
  private final static String AUTHOR_ALL = "$root.name.first$ $root.name.last$";
41
42
  /**
43
   * Test that a hierarchical relationship of {@link TreeItem} instances can
44
   * create a flat map with all string values containing key names interpolated.
45
   */
46
  @Test
47
  public void test_Resolve_ReferencesInTree_InterpolatedMap() {
48
    final var root = new TreeItem<>( "root" );
49
    final var name = new TreeItem<>( "name" );
50
    final var first = new TreeItem<>( "first" );
51
    final var authorFirst = new TreeItem<>( AUTHOR_FIRST );
52
    final var last = new TreeItem<>( "last" );
53
    final var authorLast = new TreeItem<>( AUTHOR_LAST );
54
    final var full = new TreeItem<>( "full" );
55
    final var expr = new TreeItem<>( AUTHOR_ALL );
56
57
    root.getChildren().add( name );
58
    name.getChildren().add( first );
59
    name.getChildren().add( last );
60
    name.getChildren().add( full );
61
62
    first.getChildren().add( authorFirst );
63
    last.getChildren().add( authorLast );
64
    full.getChildren().add( expr );
65
66
    final var map = TreeItemAdapter.toMap( root );
67
68
    var actualAuthor = map.get( "$root.name.full$" );
69
    var expectedAuthor = AUTHOR_ALL;
70
    assertEquals( expectedAuthor, actualAuthor );
71
72
    MapInterpolator.interpolate( map );
73
    actualAuthor = map.get( "$root.name.full$" );
74
75
    expectedAuthor = format( "%s %s", AUTHOR_FIRST, AUTHOR_LAST );
76
    assertEquals( expectedAuthor, actualAuthor );
77
  }
78
}
179