Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
A .classpath
1
<?xml version="1.0" encoding="UTF-8"?>
2
<classpath>
3
	<classpathentry kind="src" path="src/main/java">
4
		<attributes>
5
			<attribute name="FROM_GRADLE_MODEL" value="true"/>
6
		</attributes>
7
	</classpathentry>
8
	<classpathentry kind="src" path="src/main/resources">
9
		<attributes>
10
			<attribute name="FROM_GRADLE_MODEL" value="true"/>
11
		</attributes>
12
	</classpathentry>
13
	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
14
		<accessrules>
15
			<accessrule kind="accessible" pattern="javafx/**"/>
16
		</accessrules>
17
	</classpathentry>
18
	<classpathentry exported="true" kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
19
	<classpathentry kind="output" path="bin"/>
20
</classpath>
121
A .gitattributes
1
# always use LF line endings
2
3
# ALL FILES:
4
#   Normalize line endings to LF on checkin and
5
#   prevent conversion to CRLF when the file is checked out.
6
7
* text eol=lf
8
9
10
# BINARY FILES:
11
#   Disable line ending normalize on checkin.
12
13
*.gif binary
14
*.jar binary
15
*.png binary
16
*.zip binary
117
A .gitignore
1
/bin/
2
/build/
3
/.gradle/
4
/gradle/
5
/.nb-gradle
6
/private
7
.nb-gradle-properties
8
scrivenvar.pro
19
A .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/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="JDK1.8" project-jdk-type="JavaSDK">
5
    <output url="file://$PROJECT_DIR$/out" />
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 .idea/workspace.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="ChangeListManager">
4
    <list default="true" id="3dcf7c8f-87b5-4d25-a804-39da40a621b8" name="Default Changelist" comment="">
5
      <change afterPath="$PROJECT_DIR$/.idea/rGraphicsSettings.xml" afterDir="false" />
6
      <change afterPath="$PROJECT_DIR$/.idea/rpackages.xml" afterDir="false" />
7
      <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
8
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Constants.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Constants.java" afterDir="false" />
9
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/MainWindow.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/MainWindow.java" afterDir="false" />
10
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Messages.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/Messages.java" afterDir="false" />
11
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/dialogs/RScriptDialog.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/dialogs/RScriptDialog.java" afterDir="false" />
12
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/InlineRProcessor.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/InlineRProcessor.java" afterDir="false" />
13
      <change beforePath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/ProcessorFactory.java" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/java/com/scrivenvar/processors/ProcessorFactory.java" afterDir="false" />
14
      <change beforePath="$PROJECT_DIR$/src/main/resources/com/scrivenvar/messages.properties" beforeDir="false" afterPath="$PROJECT_DIR$/src/main/resources/com/scrivenvar/messages.properties" afterDir="false" />
15
    </list>
16
    <option name="SHOW_DIALOG" value="false" />
17
    <option name="HIGHLIGHT_CONFLICTS" value="true" />
18
    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
19
    <option name="LAST_RESOLUTION" value="IGNORE" />
20
  </component>
21
  <component name="ExternalProjectsData">
22
    <projectState path="$PROJECT_DIR$">
23
      <ProjectState />
24
    </projectState>
25
  </component>
26
  <component name="ExternalProjectsManager">
27
    <system id="GRADLE">
28
      <state>
29
        <task path="$PROJECT_DIR$">
30
          <activation />
31
        </task>
32
        <projects_view>
33
          <tree_state>
34
            <expand>
35
              <path>
36
                <item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
37
                <item name="scrivenvar" type="f1a62948:ProjectNode" />
38
              </path>
39
              <path>
40
                <item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
41
                <item name="scrivenvar" type="f1a62948:ProjectNode" />
42
                <item name="Tasks" type="e4a08cd1:TasksNode" />
43
              </path>
44
              <path>
45
                <item name="" type="6a2764b6:ExternalProjectsStructure$RootNode" />
46
                <item name="scrivenvar" type="f1a62948:ProjectNode" />
47
                <item name="Run Configurations" type="7b0102dc:RunConfigurationsNode" />
48
              </path>
49
            </expand>
50
            <select />
51
          </tree_state>
52
        </projects_view>
53
      </state>
54
    </system>
55
  </component>
56
  <component name="FileTemplateManagerImpl">
57
    <option name="RECENT_TEMPLATES">
58
      <list>
59
        <option value="Class" />
60
      </list>
61
    </option>
62
  </component>
63
  <component name="Git.Settings">
64
    <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
65
  </component>
66
  <component name="ProjectId" id="1bxOWN6JNt3E3hwxZwoPKSZNdNc" />
67
  <component name="ProjectLevelVcsManager" settingsEditedManually="true" />
68
  <component name="ProjectViewState">
69
    <option name="hideEmptyMiddlePackages" value="true" />
70
    <option name="showLibraryContents" value="true" />
71
  </component>
72
  <component name="PropertiesComponent">
73
    <property name="ASKED_ADD_EXTERNAL_FILES" value="true" />
74
    <property name="SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
75
    <property name="com.android.tools.idea.instantapp.provision.ProvisionBeforeRunTaskProvider.myTimeStamp" value="1541653415064" />
76
    <property name="last_opened_file_path" value="$PROJECT_DIR$" />
77
    <property name="settings.editor.selected.configurable" value="reference.settingsdialog.project.gradle" />
78
  </component>
79
  <component name="RunManager" selected="Application.Launcher">
80
    <configuration name="Launcher" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
81
      <option name="MAIN_CLASS_NAME" value="com.scrivenvar.Launcher" />
82
      <module name="scrivenvar_main" />
83
      <extension name="coverage">
84
        <pattern>
85
          <option name="PATTERN" value="com.scrivenvar.*" />
86
          <option name="ENABLED" value="true" />
87
        </pattern>
88
      </extension>
89
      <method v="2">
90
        <option name="Make" enabled="true" />
91
      </method>
92
    </configuration>
93
    <configuration name="Main" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
94
      <option name="MAIN_CLASS_NAME" value="com.scrivenvar.Main" />
95
      <module name="scrivenvar_main" />
96
      <extension name="coverage">
97
        <pattern>
98
          <option name="PATTERN" value="com.scrivenvar.*" />
99
          <option name="ENABLED" value="true" />
100
        </pattern>
101
      </extension>
102
      <method v="2">
103
        <option name="Make" enabled="true" />
104
      </method>
105
    </configuration>
106
    <configuration name="Scrivenvar" type="GradleRunConfiguration" factoryName="Gradle">
107
      <ExternalSystemSettings>
108
        <option name="executionName" />
109
        <option name="externalProjectPath" value="$PROJECT_DIR$" />
110
        <option name="externalSystemIdString" value="GRADLE" />
111
        <option name="scriptParameters" value="" />
112
        <option name="taskDescriptions">
113
          <list />
114
        </option>
115
        <option name="taskNames">
116
          <list>
117
            <option value="run" />
118
          </list>
119
        </option>
120
        <option name="vmOptions" value="" />
121
      </ExternalSystemSettings>
122
      <GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
123
      <method v="2" />
124
    </configuration>
125
    <configuration default="true" type="GradleRunConfiguration" factoryName="Gradle">
126
      <ExternalSystemSettings>
127
        <option name="executionName" />
128
        <option name="externalProjectPath" value="" />
129
        <option name="externalSystemIdString" value="GRADLE" />
130
        <option name="scriptParameters" value="" />
131
        <option name="taskDescriptions">
132
          <list />
133
        </option>
134
        <option name="taskNames">
135
          <list />
136
        </option>
137
        <option name="vmOptions" value="" />
138
      </ExternalSystemSettings>
139
      <GradleScriptDebugEnabled>true</GradleScriptDebugEnabled>
140
      <method v="2" />
141
    </configuration>
142
    <recent_temporary>
143
      <list>
144
        <item itemvalue="Application.Launcher" />
145
        <item itemvalue="Application.Main" />
146
      </list>
147
    </recent_temporary>
148
  </component>
149
  <component name="SvnConfiguration">
150
    <configuration />
151
  </component>
152
  <component name="TaskManager">
153
    <task active="true" id="Default" summary="Default task">
154
      <changelist id="3dcf7c8f-87b5-4d25-a804-39da40a621b8" name="Default Changelist" comment="" />
155
      <created>1541651873782</created>
156
      <option name="number" value="Default" />
157
      <option name="presentableId" value="Default" />
158
      <updated>1541651873782</updated>
159
    </task>
160
    <servers />
161
  </component>
162
  <component name="WindowStateProjectService">
163
    <state x="521" y="258" width="605" height="787" key="#Scrivenvar" timestamp="1589659082008">
164
      <screen x="0" y="28" width="2560" height="1529" />
165
    </state>
166
    <state x="521" y="258" width="605" height="787" key="#Scrivenvar/0.28.2560.1529@0.28.2560.1529" timestamp="1589659082008" />
167
    <state x="285" y="311" key="#com.intellij.execution.impl.EditConfigurationsDialog" timestamp="1589659128840">
168
      <screen x="0" y="28" width="2560" height="1529" />
169
    </state>
170
    <state x="285" y="311" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1589659128840" />
171
    <state x="635" y="363" width="376" height="578" key="#com.intellij.ide.util.MemberChooser" timestamp="1589658771205">
172
      <screen x="0" y="28" width="2560" height="1529" />
173
    </state>
174
    <state x="635" y="363" width="376" height="578" key="#com.intellij.ide.util.MemberChooser/0.28.2560.1529@0.28.2560.1529" timestamp="1589658771205" />
175
    <state x="468" y="28" width="711" height="1526" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog" timestamp="1589658744105">
176
      <screen x="0" y="28" width="2560" height="1529" />
177
    </state>
178
    <state x="468" y="28" width="711" height="1526" key="#com.intellij.refactoring.rename.AutomaticRenamingDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1589658744105" />
179
    <state x="610" y="411" width="426" height="481" key="FileChooserDialogImpl" timestamp="1589659107517">
180
      <screen x="0" y="28" width="2560" height="1529" />
181
    </state>
182
    <state x="610" y="411" width="426" height="481" key="FileChooserDialogImpl/0.28.2560.1529@0.28.2560.1529" timestamp="1589659107517" />
183
    <state width="1573" height="321" key="GridCell.Tab.0.bottom" timestamp="1589671859570">
184
      <screen x="0" y="28" width="2560" height="1529" />
185
    </state>
186
    <state width="1573" height="321" key="GridCell.Tab.0.bottom/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859570" />
187
    <state width="1573" height="321" key="GridCell.Tab.0.center" timestamp="1589671859569">
188
      <screen x="0" y="28" width="2560" height="1529" />
189
    </state>
190
    <state width="1573" height="321" key="GridCell.Tab.0.center/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859569" />
191
    <state width="1573" height="321" key="GridCell.Tab.0.left" timestamp="1589671859569">
192
      <screen x="0" y="28" width="2560" height="1529" />
193
    </state>
194
    <state width="1573" height="321" key="GridCell.Tab.0.left/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859569" />
195
    <state width="1573" height="321" key="GridCell.Tab.0.right" timestamp="1589671859570">
196
      <screen x="0" y="28" width="2560" height="1529" />
197
    </state>
198
    <state width="1573" height="321" key="GridCell.Tab.0.right/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859570" />
199
    <state width="1573" height="396" key="GridCell.Tab.1.bottom" timestamp="1589671859555">
200
      <screen x="0" y="28" width="2560" height="1529" />
201
    </state>
202
    <state width="1573" height="396" key="GridCell.Tab.1.bottom/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859555" />
203
    <state width="1573" height="396" key="GridCell.Tab.1.center" timestamp="1589671859555">
204
      <screen x="0" y="28" width="2560" height="1529" />
205
    </state>
206
    <state width="1573" height="396" key="GridCell.Tab.1.center/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859555" />
207
    <state width="1573" height="396" key="GridCell.Tab.1.left" timestamp="1589671859554">
208
      <screen x="0" y="28" width="2560" height="1529" />
209
    </state>
210
    <state width="1573" height="396" key="GridCell.Tab.1.left/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859554" />
211
    <state width="1573" height="396" key="GridCell.Tab.1.right" timestamp="1589671859555">
212
      <screen x="0" y="28" width="2560" height="1529" />
213
    </state>
214
    <state width="1573" height="396" key="GridCell.Tab.1.right/0.28.2560.1529@0.28.2560.1529" timestamp="1589671859555" />
215
    <state x="324" y="288" key="SettingsEditor" timestamp="1589576619807">
216
      <screen x="0" y="28" width="2560" height="1529" />
217
    </state>
218
    <state x="324" y="288" key="SettingsEditor/0.28.2560.1529@0.28.2560.1529" timestamp="1589576619807" />
219
    <state x="1071" y="397" width="1417" height="979" key="com.intellij.history.integration.ui.views.FileHistoryDialog" timestamp="1589661186060">
220
      <screen x="0" y="28" width="2560" height="1529" />
221
    </state>
222
    <state x="1071" y="397" width="1417" height="979" key="com.intellij.history.integration.ui.views.FileHistoryDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1589661186060" />
223
    <state x="531" y="261" width="586" height="753" key="find.popup" timestamp="1589669468040">
224
      <screen x="0" y="28" width="2560" height="1529" />
225
    </state>
226
    <state x="531" y="261" width="586" height="753" key="find.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1589669468040" />
227
    <state x="533" y="414" width="581" height="476" key="refactoring.ChangeSignatureDialog" timestamp="1589663937037">
228
      <screen x="0" y="28" width="2560" height="1529" />
229
    </state>
230
    <state x="533" y="414" width="581" height="476" key="refactoring.ChangeSignatureDialog/0.28.2560.1529@0.28.2560.1529" timestamp="1589663937037" />
231
    <state x="490" y="304" key="run.anything.popup" timestamp="1589657324666">
232
      <screen x="0" y="28" width="2560" height="1529" />
233
    </state>
234
    <state x="490" y="304" key="run.anything.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1589657324666" />
235
    <state x="490" y="327" width="672" height="678" key="search.everywhere.popup" timestamp="1589669442714">
236
      <screen x="0" y="28" width="2560" height="1529" />
237
    </state>
238
    <state x="490" y="327" width="672" height="678" key="search.everywhere.popup/0.28.2560.1529@0.28.2560.1529" timestamp="1589669442714" />
239
  </component>
240
  <component name="XDebuggerManager">
241
    <breakpoint-manager>
242
      <breakpoints>
243
        <line-breakpoint enabled="true" type="java-line">
244
          <url>file://$PROJECT_DIR$/src/main/java/com/scrivenvar/MainWindow.java</url>
245
          <line>410</line>
246
          <option name="timeStamp" value="4" />
247
        </line-breakpoint>
248
      </breakpoints>
249
    </breakpoint-manager>
250
  </component>
251
  <component name="masterDetails">
252
    <states>
253
      <state key="ProjectJDKs.UI">
254
        <settings>
255
          <last-edited>JDK1.8</last-edited>
256
          <splitter-proportions>
257
            <option name="proportions">
258
              <list>
259
                <option value="0.2" />
260
              </list>
261
            </option>
262
          </splitter-proportions>
263
        </settings>
264
      </state>
265
    </states>
266
  </component>
267
</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
# Build
2
3
This document describes how to build the application.
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
# Compile
13
14
Build the application 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
130
A CREDITS.md
1
# Credits
2
3
* Dave Jarvis: [Scrivenvar](https://github.com/DaveJarvis/scrivenvar/)
4
* Karl Tauber: [Markdown Writer FX](https://github.com/JFormDesigner/markdown-writer-fx)
5
* 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)
6
* Mikael Grev: [MigLayout](http://www.miglayout.com/)
7
* Tom Eugelink: [MigPane](https://github.com/mikaelgrev/miglayout/blob/master/javafx/src/main/java/org/tbee/javafx/scene/layout/fxml/MigPane.java)
8
* Vladimir Schneider: [flexmark](https://website.com)
9
* Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
10
* Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet)
11
* David Croft, [File Preferences](https://github.com/eis/simple-suomi24-java-client/tree/master/src/main/java/net/infotrek/util/prefs)
12
* Alex Bertram, [Renjin](https://www.renjin.org/)
13
* Michael Kay, [XSLT Processor](http://www.saxonica.com/)
14
115
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
4
Import the files in this directory into the application, which include:
5
6
* pluralise.R
7
* possessive.R
8
9
pluralise.R
10
===
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 pluralise function include:
17
18
    `r#pluralise( 'mouse' )` - mice
19
    `r#pluralise( 'buzz' )` - buzzes
20
    `r#pluralise( 'bus' )` - busses
21
22
possessive.R
23
===
24
25
This file defines a function that applies possessives to English words.
26
27
Usage
28
---
29
Example usages of the possessive function include:
30
31
    `r#pos( 'Ross' )` - Ross'
32
    `r#pos( 'Ruby' )` - Ruby's
33
    `r#pos( 'Lois' )` - Lois'
34
135
A R/pluralise.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
pluralise <- 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( "es", s ) ||
43
        pl.suffix( "fish", s ) ||
44
        pl.suffix( "ois", s ) ||
45
        pl.suffix( "sheep", s ) ||
46
        pl.suffix( "deer", s ) ||
47
        pl.suffix( "pox", s ) ||
48
        pl.suffix( "[A-Z].*ese", s ) ||
49
        pl.suffix( "itis", s ) ) {
50
      # 1. Retain non-inflective user-mapped noun as is.
51
      # 2. Retain non-inflective plural as is.
52
      result <- s
53
    }
54
    else if( pl.is.irregular.pl( s ) ) {
55
      # 4. Change irregular plurals based on mapping.
56
      result <- pl.irregular.pl( s )
57
    }
58
    else if( pl.is.irregular.es( s ) ) {
59
      # x. From Shevek's
60
      result <- pl.inflect( s, "", "es" )
61
    }
62
    else if( pl.suffix( "man", s ) ) {
63
      # 5. For -man, change -an to -en
64
      result <- pl.inflect( s, "an", "en" )
65
    }
66
    else if( pl.suffix( "[lm]ouse", s ) ) {
67
      # 5. For [lm]ouse, change -ouse to -ice
68
      result <- pl.inflect( s, "ouse", "ice" )
69
    }
70
    else if( pl.suffix( "tooth", s ) ) {
71
      # 5. For -tooth, change -ooth to -eeth
72
      result <- pl.inflect( s, "ooth", "eeth" )
73
    }
74
    else if( pl.suffix( "goose", s ) ) {
75
      # 5. For -goose, change -oose to -eese
76
      result <- pl.inflect( s, "oose", "eese" )
77
    }
78
    else if( pl.suffix( "foot", s ) ) {
79
      # 5. For -foot, change -oot to -eet
80
      result <- pl.inflect( s, "oot", "eet" )
81
    }
82
    else if( pl.suffix( "zoon", s ) ) {
83
      # 5. For -zoon, change -on to -a
84
      result <- pl.inflect( s, "on", "a" )
85
    }
86
    else if( pl.suffix( "[csx]is", s ) ) {
87
      # 5. Change -cis, -sis, -xis to -es
88
      result <- pl.inflect( s, "is", "es" )
89
    }
90
    else if( pl.suffix( "([cs]h|ss|zz|x|s)", s ) ) {
91
      # 8. Change -ch, -sh, -ss, -zz, -x, -s to -es
92
      result <- pl.inflect( s, "", "es" )
93
    }
94
    else if( pl.suffix( "([aeo]lf|[^d]eaf|arf)", s ) ) {
95
      # 9. Change -f to -ves
96
      result <- pl.inflect( s, "f", "ves" )
97
    }
98
    else if( pl.suffix( "[nlw]ife", s ) ) {
99
      # 10. Change -fe to -ves
100
      result <- pl.inflect( s, "fe", "ves" )
101
    }
102
    else if( pl.suffix( "[aeiou]y", s ) ) {
103
      # 11. Change -[aeiou]y to -ys
104
      result <- pl.inflect( s, "", "s" )
105
    }
106
    else if( pl.suffix( "y", s ) ) {
107
      # 12. Change -y to -ies
108
      result <- pl.inflect( s, "y", "ies" )
109
    }
110
    else if( pl.suffix( "z", s ) ) {
111
      # x. Change -z to -zzes
112
      result <- pl.inflect( s, "", "zes" )
113
    }
114
    else {
115
      # 13. Default plural: add -s
116
      result <- pl.inflect( s, "", "s" )
117
    }
118
  }
119
120
  result
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",
137
    "Bhutanese",
138
    "bison",
139
    "bream",
140
    "Burmese",
141
    "carp",
142
    "chassis",
143
    "Chinese",
144
    "clippers",
145
    "cod",
146
    "contretemps",
147
    "corps",
148
    "debris",
149
    "djinn",
150
    "eland",
151
    "elk",
152
    "flounder",
153
    "fracas",
154
    "gallows",
155
    "graffiti",
156
    "headquarters",
157
    "high-jinks",
158
    "homework",
159
    "hovercraft",
160
    "innings",
161
    "Japanese",
162
    "Lebanese",
163
    "mackerel",
164
    "means",
165
    "mews",
166
    "mice",
167
    "mumps",
168
    "news",
169
    "pincers",
170
    "pliers",
171
    "Portuguese",
172
    "proceedings",
173
    "salmon",
174
    "scissors",
175
    "sea-bass",
176
    "Senegalese",
177
    "shears",
178
    "Siamese",
179
    "Sinhalese",
180
    "spacecraft",
181
    "swine",
182
    "trout",
183
    "tuna",
184
    "Vietnamese",
185
    "watercraft",
186
    "whiting",
187
    "wildebeest"
188
  )
189
190
  is.element( s, v )
191
}
192
193
# Answers whether the given string (s) is an irregular plural.
194
pl.is.irregular.pl <- function( s ) {
195
  # Could be refactored with pl.irregular.pl...
196
  v <- c(
197
    "beef", "brother", "child", "cow", "ephemeris", "genie", "money",
198
    "mongoose", "mythos", "octopus", "ox", "soliloquy", "trilby"
199
  )
200
201
  is.element( s, v )
202
}
203
204
# Call to pluralise an irregular noun. Only call after confirming
205
# the noun is irregular via pl.is.irregular.pl.
206
pl.irregular.pl <- function( s ) {
207
  v <- list(
208
    "beef" = "beefs",
209
    "brother" = "brothers",
210
    "child" = "children",
211
    "cow" = "cows",
212
    "ephemeris" = "ephemerides",
213
    "genie" = "genies",
214
    "money" = "moneys",
215
    "mongoose" = "mongooses",
216
    "mythos" = "mythoi",
217
    "octopus" = "octopuses",
218
    "ox" = "oxen",
219
    "soliloquy" = "soliloquies",
220
    "trilby" = "trilbys"
221
  )
222
223
  # Faster version of v[[ s ]]
224
  .subset2( v, s )
225
}
226
227
# Answers whether the given string (s) pluralises with -es.
228
pl.is.irregular.es <- function( s ) {
229
  v <- c(
230
    "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis",
231
    "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais",
232
    "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris",
233
    "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis",
234
    "polis", "rhinoceros", "sassafrass", "trellis"
235
  )
236
237
  is.element( s, v )
238
}
239
1240
A R/possessive.R
1
# ######################################################################
2
#
3
# Copyright 2017, 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
# Changes a word into its possessive form.
29
#
30
# ######################################################################
31
32
# Returns leftmost n characters of s.
33
lstr <- function( s, n = 1 ) {
34
  substr( s, 0, n )
35
}
36
37
# Returns rightmost n characters of s.
38
rstr <- function( s, n = 1 ) {
39
  l = nchar( s )
40
  substr( s, l - n + 1, l )
41
}
42
43
# Returns the possessive form of the given word.
44
pos <- function( s ) {
45
  result <- s
46
47
  # Check to see if the last character is an s.
48
  ch <- rstr( s, 1 )
49
50
  if( ch != "s" ) {
51
    result <- concat( result, "'s" )
52
  }
53
  else {
54
    result <- concat( result, "'" )
55
  }
56
57
  result
58
}
59
160
A README.md
1
![Logo](images/logo64.png)
2
3
# $application.title$
4
5
Text editing using interpolated strings.
6
7
## Requirements
8
9
Download and install the following software packages:
10
11
* [OpenJDK 14](https://openjdk.java.net)
12
13
## Quick Start
14
15
Complete the following steps to run the application:
16
17
1. [Download](https://github.com/DaveJarvis/scrivenvar/releases)
18
`scrivenvar.jar`.
19
1. Double-click `scrivenvar.jar` to start the application.
20
21
## Command Line Start
22
23
If the quick start fails, run the application as follows:
24
25
1. Open a command prompt.
26
1. Change to the download directory containing the archive file.
27
1. Run: `java -jar scrivenvar.jar`
28
29
## Features
30
31
* R integration
32
* User-defined variables, interpolated
33
* Real-time preview with variable substitution
34
* Auto-complete variable names based on variable values
35
* XML document transformation using XSLT3 or older
36
* Platform independent (Windows, Linux, MacOS)
37
38
## Future Features
39
40
* Spell check
41
* Search and replace using variables
42
* Re-organize variable names
43
44
## Screenshot
45
46
![Screenshot](images/screenshot.png)
47
48
## License
49
50
This software is licensed under the [BSD 2-Clause License](LICENSE.md).
51
152
A USAGE-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
        if( is.atomic( r ) ) { r }
158
        else { s }
159
      },
160
      warning = function( w ) { s },
161
      error = function( e ) { s })
162
    }
163
    ```
164
1. Click **OK**.
165
1. Close and re-open `sum.Rmd`.
166
167
The preview panel shows:
168
169
```
170
25.0
171
172
Project Title
173
```
174
175
The `x` function attempts to evaluate the expression defined by the YAML
176
variable. This means that the YAML definitions can also include expressions
177
that R is capable of evaluating.
178
179
While the `x` function can be defined within the R Startup Script, it is
180
better practice to put it into its own library so that it can be reused
181
outside of the application.
182
1183
A USAGE.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 _config.yaml
1
application:
2
  title: Scrivenvar
3
14
A build.gradle
1
plugins {
2
  id 'application'
3
  id 'org.openjfx.javafxplugin' version '0.0.8'
4
}
5
6
repositories {
7
  mavenCentral()
8
  jcenter()
9
10
  maven {
11
    url 'https://oss.sonatype.org/content/repositories/snapshots/'
12
  }
13
14
  maven {
15
    url "https://nexus.bedatadriven.com/content/groups/public"
16
  }
17
}
18
19
dependencies {
20
  implementation 'org.controlsfx:controlsfx:11.0.0'
21
  implementation 'org.fxmisc.richtext:richtextfx:0.10.5'
22
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
23
  implementation 'com.miglayout:miglayout-javafx:5.2'
24
  implementation 'com.vladsch.flexmark:flexmark:0.61.28'
25
  implementation 'com.vladsch.flexmark:flexmark-ext-tables:0.61.28'
26
  implementation 'com.vladsch.flexmark:flexmark-ext-superscript:0.61.28'
27
  implementation 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.61.28'
28
  implementation 'com.fasterxml.jackson.core:jackson-core:2.11.0'
29
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0'
30
  implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.0'
31
  implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.0'
32
  implementation 'org.ahocorasick:ahocorasick:0.4.0'
33
  implementation 'org.yaml:snakeyaml:1.26'
34
  implementation 'com.ximpleware:vtd-xml:2.13.4'
35
  implementation 'net.sf.saxon:Saxon-HE:10.1'
36
  implementation 'org.apache.commons:commons-configuration2:2.7'
37
  implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
38
  implementation 'de.jensd:fontawesomefx-commons:11.0'
39
  implementation 'de.jensd:fontawesomefx-fontawesome:4.7.0-11'
40
  implementation "org.renjin:renjin-script-engine:0.9.2726"
41
42
  def os = ['win', 'linux', 'mac']
43
  def fx = ['controls', 'graphics', 'web', 'fxml']
44
45
  fx.each { fxitem ->
46
    os.each { ositem ->
47
      runtimeOnly "org.openjfx:javafx-${fxitem}:${javafx.version}:${ositem}"
48
    }
49
  }
50
}
51
52
javafx {
53
  version = "14"
54
  modules = ['javafx.controls', 'javafx.graphics', 'javafx.web']
55
}
56
57
compileJava {
58
  options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
59
}
60
61
sourceCompatibility = JavaVersion.VERSION_11
62
version = '1.4.0'
63
applicationName = 'scrivenvar'
64
mainClassName = 'com.scrivenvar.Main'
65
def launcherClassName = 'com.scrivenvar.Launcher'
66
67
jar {
68
  duplicatesStrategy = DuplicatesStrategy.EXCLUDE
69
70
  manifest {
71
    attributes 'Main-Class': launcherClassName
72
  }
73
74
  from {
75
    (configurations.runtimeClasspath.findAll { !it.path.endsWith(".pom") }).collect {
76
      it.isDirectory() ? it : zipTree(it)
77
    }
78
  }
79
80
  archiveFileName = 'scrivenvar.jar'
81
82
  exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
83
}
84
85
distributions {
86
  main {
87
    distributionBaseName = applicationName
88
    contents {
89
      from { ['LICENSE.md', 'README.md'] }
90
      into('images') {
91
        from { 'images' }
92
      }
93
    }
94
  }
95
}
196
A gradle.properties
1
org.gradle.jvmargs=-Xmx1G -XX:MaxPermSize=512m
2
13
A images/logo64.png
Binary file
A images/screenshot.png
Binary file
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 settings.gradle
11
A src/main/java/com/scrivenvar/AbstractFileFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import 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 java.lang.String.format;
39
40
/**
41
 * Provides common behaviours for factories that instantiate classes based on
42
 * file type.
43
 *
44
 * @author White Magic Software, Ltd.
45
 */
46
public class AbstractFileFactory {
47
48
  private static final String MSG_UNKNOWN_FILE_TYPE = "Unknown type '%s' for " +
49
      "file '%s'.";
50
51
  private final Settings mSettings = Services.load( Settings.class );
52
53
  /**
54
   * Determines the file type from the path extension. This should only be
55
   * called when it is known that the file type won't be a definition file
56
   * (e.g., YAML or other definition source), but rather an editable file
57
   * (e.g., Markdown, XML, etc.).
58
   *
59
   * @param path The path with a file name extension.
60
   * @return The FileType for the given path.
61
   */
62
  public FileType lookup( final Path path ) {
63
    return lookup( path, GLOB_PREFIX_FILE );
64
  }
65
66
  /**
67
   * Creates a file type that corresponds to the given path.
68
   *
69
   * @param path   Reference to a variable definition file.
70
   * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
71
   * @return The file type that corresponds to the given path.
72
   */
73
  protected FileType lookup( final Path path, final String prefix ) {
74
    assert path != null;
75
    assert prefix != null;
76
77
    final Settings properties = getSettings();
78
    final Iterator<String> keys = properties.getKeys( prefix );
79
80
    boolean found = false;
81
    FileType fileType = null;
82
83
    while( keys.hasNext() && !found ) {
84
      final String key = keys.next();
85
      final List<String> patterns = properties.getStringSettingList( key );
86
      final FileTypePredicate predicate = new FileTypePredicate( patterns );
87
88
      if( found = predicate.test( path.toFile() ) ) {
89
        // Remove the EXTENSIONS_PREFIX to get the filename extension mapped
90
        // to a standard name (as defined in the settings.properties file).
91
        final String suffix = key.replace( prefix + ".", "" );
92
        fileType = FileType.from( suffix );
93
      }
94
    }
95
96
    if( fileType == null ) {
97
      unknownFileType( fileType, path );
98
    }
99
100
    return fileType;
101
  }
102
103
  /**
104
   * Throws IllegalArgumentException because the given path could not be
105
   * recognized.
106
   *
107
   * @param type The detected path type (protocol, file extension, etc.).
108
   * @param path The path to a source of definitions.
109
   */
110
  protected void unknownFileType( final FileType type, final Path path ) {
111
    final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path );
112
    throw new IllegalArgumentException( msg );
113
  }
114
115
  /**
116
   * Throws IllegalArgumentException because the given path could not be
117
   * recognized. This exists because
118
   *
119
   * @param type The detected path type (protocol, file extension, etc.).
120
   * @param path The path to a source of definitions.
121
   */
122
  protected void unknownFileType( final String type, final String path ) {
123
    final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path );
124
    throw new IllegalArgumentException( msg );
125
  }
126
127
  /**
128
   * Return the singleton Settings instance.
129
   *
130
   * @return A non-null instance.
131
   */
132
  private Settings getSettings() {
133
    return this.mSettings;
134
  }
135
}
1136
A src/main/java/com/scrivenvar/AbstractPane.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.service.Options;
31
32
import java.util.prefs.Preferences;
33
34
import org.tbee.javafx.scene.layout.fxml.MigPane;
35
36
/**
37
 * Provides options to all subclasses.
38
 *
39
 * @author White Magic Software, Ltd.
40
 */
41
public abstract class AbstractPane extends MigPane {
42
43
  /**
44
   * The options loaded from the service.
45
   */
46
  private final Options options = Services.load( Options.class );
47
48
  /**
49
   * Returns the persistent options for user settings.
50
   *
51
   * @return A non-null instance.
52
   */
53
  protected Options getOptions() {
54
    return this.options;
55
  }
56
57
  /**
58
   * Returns a hierarchical set of preferences.
59
   *
60
   * @return A non-null instance.
61
   */
62
  protected Preferences getState() {
63
    return getOptions().getState();
64
  }
65
}
166
A src/main/java/com/scrivenvar/Constants.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.service.Settings;
31
32
import java.util.Collection;
33
34
/**
35
 * Defines application-wide default values.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public class Constants {
40
41
  private static final Settings SETTINGS = Services.load( Settings.class );
42
43
  /**
44
   * Prevent instantiation, deliberately.
45
   */
46
  private Constants() {
47
  }
48
49
  private static String get( final String key ) {
50
    return SETTINGS.getSetting( key, "" );
51
  }
52
53
  @SuppressWarnings("SameParameterValue")
54
  private static int get( final String key, final int defaultValue ) {
55
    return SETTINGS.getSetting( key, defaultValue );
56
  }
57
58
  @SuppressWarnings("SameParameterValue")
59
  private static Collection<String> getStringSettingList( final String key ) {
60
    return SETTINGS.getStringSettingList( key );
61
  }
62
63
  // Bootstrapping...
64
  public static final String SETTINGS_NAME = "/com/scrivenvar/settings.properties";
65
66
  public static final String APP_TITLE = get( "application.title" );
67
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
68
69
  // Prevent double events when updating files on Linux (save and timestamp).
70
  public static final int APP_WATCHDOG_TIMEOUT = get( "application.watchdog.timeout", 100 );
71
72
  public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
73
  public static final String STYLESHEET_MARKDOWN = get( "file.stylesheet.markdown" );
74
  public static final String STYLESHEET_PREVIEW = get( "file.stylesheet.preview" );
75
76
  public static final String FILE_LOGO_16 = get( "file.logo.16" );
77
  public static final String FILE_LOGO_32 = get( "file.logo.32" );
78
  public static final String FILE_LOGO_128 = get( "file.logo.128" );
79
  public static final String FILE_LOGO_256 = get( "file.logo.256" );
80
  public static final String FILE_LOGO_512 = get( "file.logo.512" );
81
82
  public static final String CARET_POSITION_BASE = get( "caret.token.base" );
83
  public static final String CARET_POSITION_MD = get( "caret.token.markdown" );
84
  public static final String CARET_POSITION_HTML = get( "caret.token.html" );
85
86
  public static final String PREFS_ROOT = get( "preferences.root" );
87
  public static final String PREFS_STATE = get( "preferences.root.state" );
88
  public static final String PREFS_OPTIONS = get( "preferences.root.options" );
89
90
  // Refer to filename extension settings in the configuration file. Do not
91
  // terminate these prefixes with a period.
92
  public static final String GLOB_PREFIX_FILE = "file.ext";
93
  public static final String GLOB_PREFIX_DEFINITION = "definition." + GLOB_PREFIX_FILE;
94
95
  public static final Collection<String> GLOB_DEFINITION_EXTENSIONS
96
    = getStringSettingList( GLOB_PREFIX_FILE + ".definition" );
97
98
  // Different definition source protocols.
99
  public static final String DEFINITION_PROTOCOL_UNKNOWN = "unknown";
100
  public static final String DEFINITION_PROTOCOL_FILE = "file";
101
102
  // Takes two parameters: line number and column number.
103
  public static final String STATUS_BAR_LINE = "Main.statusbar.line";
104
105
  // "OK" text
106
  public static final String STATUS_BAR_DEFAULT = get( "Main.statusbar.state.default" );
107
  public static final String STATUS_PARSE_ERROR = "Main.statusbar.parse.error";
108
109
  /**
110
   * Location of the definition source file.
111
   */
112
  public static final String PERSIST_DEFINITION_SOURCE = "definitionSource";
113
114
  /**
115
   * Content of the R startup script.
116
   */
117
  public static final String PERSIST_R_STARTUP = "rStartup";
118
119
  /**
120
   * Bootstrap directory for R startup script.
121
   */
122
  public static final String PERSIST_R_DIRECTORY = "rDirectory";
123
124
  /**
125
   * Default working directory to use for R startup script.
126
   */
127
  public static final String USER_DIRECTORY = System.getProperty( "user.dir" );
128
}
1129
A src/main/java/com/scrivenvar/FileEditorTab.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * Redistribution and use in source and binary forms, with or without
5
 * modification, are permitted provided that the following conditions are met:
6
 *
7
 *  o Redistributions of source code must retain the above copyright
8
 *    notice, this list of conditions and the following disclaimer.
9
 *
10
 *  o Redistributions in binary form must reproduce the above copyright
11
 *    notice, this list of conditions and the following disclaimer in the
12
 *    documentation and/or other materials provided with the distribution.
13
 *
14
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
 */
26
package com.scrivenvar;
27
28
import com.scrivenvar.editors.EditorPane;
29
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
30
import com.scrivenvar.service.events.Notification;
31
import com.scrivenvar.service.events.Notifier;
32
import javafx.application.Platform;
33
import javafx.beans.binding.Bindings;
34
import javafx.beans.property.BooleanProperty;
35
import javafx.beans.property.ReadOnlyBooleanProperty;
36
import javafx.beans.property.ReadOnlyBooleanWrapper;
37
import javafx.beans.property.SimpleBooleanProperty;
38
import javafx.beans.value.ChangeListener;
39
import javafx.beans.value.ObservableValue;
40
import javafx.event.Event;
41
import javafx.scene.Node;
42
import javafx.scene.Scene;
43
import javafx.scene.control.Tab;
44
import javafx.scene.control.Tooltip;
45
import javafx.scene.input.InputEvent;
46
import javafx.scene.text.Text;
47
import javafx.stage.Window;
48
import org.fxmisc.richtext.StyleClassedTextArea;
49
import org.fxmisc.richtext.model.TwoDimensional.Position;
50
import org.fxmisc.undo.UndoManager;
51
import org.fxmisc.wellbehaved.event.EventPattern;
52
import org.fxmisc.wellbehaved.event.InputMap;
53
import org.mozilla.universalchardet.UniversalDetector;
54
55
import java.io.IOException;
56
import java.nio.charset.Charset;
57
import java.nio.file.Files;
58
import java.nio.file.Path;
59
import java.util.function.Consumer;
60
61
import static java.nio.charset.StandardCharsets.UTF_8;
62
import static java.util.Locale.ENGLISH;
63
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
64
65
/**
66
 * Editor for a single file.
67
 *
68
 * @author Karl Tauber and White Magic Software, Ltd.
69
 */
70
public final class FileEditorTab extends Tab {
71
72
  /**
73
   *
74
   */
75
  private final Notifier alertService = Services.load( Notifier.class );
76
  private EditorPane editorPane;
77
78
  /**
79
   * Character encoding used by the file (or default encoding if none found).
80
   */
81
  private Charset encoding;
82
83
  private final ReadOnlyBooleanWrapper modified = new ReadOnlyBooleanWrapper();
84
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
85
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
86
87
  private Path path;
88
89
  public FileEditorTab( final Path path ) {
90
    setPath( path );
91
92
    this.modified.addListener( ( observable, oldPath, newPath ) -> updateTab() );
93
94
    setOnSelectionChanged( e -> {
95
      if( isSelected() ) {
96
        Platform.runLater( this::activated );
97
      }
98
    } );
99
  }
100
101
  private void updateTab() {
102
    setText( getTabTitle() );
103
    setGraphic( getModifiedMark() );
104
    setTooltip( getTabTooltip() );
105
  }
106
107
  /**
108
   * Returns the base filename (without the directory names).
109
   *
110
   * @return The untitled text if the path hasn't been set.
111
   */
112
  private String getTabTitle() {
113
    final Path filePath = getPath();
114
115
    return (filePath == null)
116
        ? Messages.get( "FileEditor.untitled" )
117
        : filePath.getFileName().toString();
118
  }
119
120
  /**
121
   * Returns the full filename represented by the path.
122
   *
123
   * @return The untitled text if the path hasn't been set.
124
   */
125
  private Tooltip getTabTooltip() {
126
    final Path filePath = getPath();
127
    return new Tooltip( filePath == null ? "" : filePath.toString() );
128
  }
129
130
  /**
131
   * Returns a marker to indicate whether the file has been modified.
132
   *
133
   * @return "*" when the file has changed; otherwise null.
134
   */
135
  private Text getModifiedMark() {
136
    return isModified() ? new Text( "*" ) : null;
137
  }
138
139
  /**
140
   * Called when the user switches tab.
141
   */
142
  private void activated() {
143
    // Tab is closed or no longer active.
144
    if( getTabPane() == null || !isSelected() ) {
145
      return;
146
    }
147
148
    // Switch to the tab without loading if the contents are already in memory.
149
    if( getContent() != null ) {
150
      getEditorPane().requestFocus();
151
      return;
152
    }
153
154
    // Load the text and update the preview before the undo manager.
155
    load();
156
157
    // Track undo requests -- can only be called *after* load.
158
    initUndoManager();
159
    initLayout();
160
    initFocus();
161
  }
162
163
  private void initLayout() {
164
    setContent( getScrollPane() );
165
  }
166
167
  private Node getScrollPane() {
168
    return getEditorPane().getScrollPane();
169
  }
170
171
  private void initFocus() {
172
    getEditorPane().requestFocus();
173
  }
174
175
  private void initUndoManager() {
176
    final UndoManager undoManager = getUndoManager();
177
178
    // Clear undo history after first load.
179
    undoManager.forgetHistory();
180
181
    // Bind the editor undo manager to the properties.
182
    modified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
183
    canUndo.bind( undoManager.undoAvailableProperty() );
184
    canRedo.bind( undoManager.redoAvailableProperty() );
185
  }
186
187
  /**
188
   * Searches from the caret position forward for the given string.
189
   *
190
   * @param needle The text string to match.
191
   */
192
  public void searchNext( final String needle ) {
193
    final String haystack = getEditorText();
194
    int index = haystack.indexOf( needle, getCaretPosition() );
195
196
    // Wrap around.
197
    if( index == -1 ) {
198
      index = haystack.indexOf( needle, 0 );
199
    }
200
201
    if( index >= 0 ) {
202
      setCaretPosition( index );
203
      getEditor().selectRange( index, index + needle.length() );
204
    }
205
  }
206
207
  /**
208
   * Returns the index into the text where the caret blinks happily away.
209
   *
210
   * @return A number from 0 to the editor's document text length.
211
   */
212
  public int getCaretPosition() {
213
    return getEditor().getCaretPosition();
214
  }
215
216
  /**
217
   * Moves the caret to a given offset.
218
   *
219
   * @param offset The new caret offset.
220
   */
221
  private void setCaretPosition( final int offset ) {
222
    getEditor().moveTo( offset );
223
    getEditor().requestFollowCaret();
224
  }
225
226
  /**
227
   * Returns the caret's current row and column position.
228
   *
229
   * @return The caret's offset into the document.
230
   */
231
  public Position getCaretOffset() {
232
    return getEditor().offsetToPosition( getCaretPosition(), Forward );
233
  }
234
235
  /**
236
   * Allows observers to synchronize caret position changes.
237
   *
238
   * @return An observable caret property value.
239
   */
240
  public final ObservableValue<Integer> caretPositionProperty() {
241
    return getEditor().caretPositionProperty();
242
  }
243
244
  /**
245
   * Returns the text area associated with this tab.
246
   *
247
   * @return A text editor.
248
   */
249
  private StyleClassedTextArea getEditor() {
250
    return getEditorPane().getEditor();
251
  }
252
253
  /**
254
   * Returns true if the given path exactly matches this tab's path.
255
   *
256
   * @param check The path to compare against.
257
   * @return true The paths are the same.
258
   */
259
  public boolean isPath( final Path check ) {
260
    final Path filePath = getPath();
261
262
    return filePath != null && filePath.equals( check );
263
  }
264
265
  /**
266
   * Reads the entire file contents from the path associated with this tab.
267
   */
268
  private void load() {
269
    final Path filePath = getPath();
270
271
    if( filePath != null ) {
272
      try {
273
        getEditorPane().setText( asString( Files.readAllBytes( filePath ) ) );
274
        getEditorPane().scrollToTop();
275
      } catch( final Exception ex ) {
276
        getNotifyService().notify( ex );
277
      }
278
    }
279
  }
280
281
  /**
282
   * Saves the entire file contents from the path associated with this tab.
283
   *
284
   * @return true The file has been saved.
285
   */
286
  public boolean save() {
287
    try {
288
      final EditorPane editor = getEditorPane();
289
      Files.write( getPath(), asBytes( editor.getText() ) );
290
      editor.getUndoManager().mark();
291
      return true;
292
    } catch( final IOException ex ) {
293
      return alert(
294
          "FileEditor.saveFailed.title", "FileEditor.saveFailed.message", ex
295
      );
296
    }
297
  }
298
299
  /**
300
   * Creates an alert dialog and waits for it to close.
301
   *
302
   * @param titleKey   Resource bundle key for the alert dialog title.
303
   * @param messageKey Resource bundle key for the alert dialog message.
304
   * @param e          The unexpected happening.
305
   * @return false
306
   */
307
  private boolean alert(
308
      final String titleKey, final String messageKey, final Exception e ) {
309
    final Notifier service = getNotifyService();
310
    final Path filePath = getPath();
311
312
    final Notification message = service.createNotification(
313
        Messages.get( titleKey ),
314
        Messages.get( messageKey ),
315
        filePath == null ? "" : filePath,
316
        e.getMessage()
317
    );
318
319
    try {
320
      service.createError( getWindow(), message ).showAndWait();
321
    } catch( final Exception ex ) {
322
      getNotifyService().notify( ex );
323
    }
324
325
    return false;
326
  }
327
328
  private Window getWindow() {
329
    final Scene scene = getEditorPane().getScene();
330
331
    if( scene == null ) {
332
      throw new UnsupportedOperationException( "" );
333
    }
334
335
    return scene.getWindow();
336
  }
337
338
  /**
339
   * Returns a best guess at the file encoding. If the encoding could not be
340
   * detected, this will return the default charset for the JVM.
341
   *
342
   * @param bytes The bytes to perform character encoding detection.
343
   * @return The character encoding.
344
   */
345
  private Charset detectEncoding( final byte[] bytes ) {
346
    final UniversalDetector detector = new UniversalDetector( null );
347
    detector.handleData( bytes, 0, bytes.length );
348
    detector.dataEnd();
349
350
    final String charset = detector.getDetectedCharset();
351
    final Charset charEncoding = charset == null
352
        ? Charset.defaultCharset()
353
        : Charset.forName( charset.toUpperCase( ENGLISH ) );
354
355
    detector.reset();
356
357
    return charEncoding;
358
  }
359
360
  /**
361
   * Converts the given string to an array of bytes using the encoding that was
362
   * originally detected (if any) and associated with this file.
363
   *
364
   * @param text The text to convert into the original file encoding.
365
   * @return A series of bytes ready for writing to a file.
366
   */
367
  private byte[] asBytes( final String text ) {
368
    return text.getBytes( getEncoding() );
369
  }
370
371
  /**
372
   * Converts the given bytes into a Java String. This will call setEncoding
373
   * with the encoding detected by the CharsetDetector.
374
   *
375
   * @param text The text of unknown character encoding.
376
   * @return The text, in its auto-detected encoding, as a String.
377
   */
378
  private String asString( final byte[] text ) {
379
    setEncoding( detectEncoding( text ) );
380
    return new String( text, getEncoding() );
381
  }
382
383
  public Path getPath() {
384
    return this.path;
385
  }
386
387
  public void setPath( final Path path ) {
388
    this.path = path;
389
390
    updateTab();
391
  }
392
393
  /**
394
   * Answers whether this tab has an initialized path reference.
395
   *
396
   * @return false This tab has no path.
397
   */
398
  public boolean isFileOpen() {
399
    return this.path != null;
400
  }
401
402
  public boolean isModified() {
403
    return this.modified.get();
404
  }
405
406
  ReadOnlyBooleanProperty modifiedProperty() {
407
    return this.modified.getReadOnlyProperty();
408
  }
409
410
  BooleanProperty canUndoProperty() {
411
    return this.canUndo;
412
  }
413
414
  BooleanProperty canRedoProperty() {
415
    return this.canRedo;
416
  }
417
418
  private UndoManager getUndoManager() {
419
    return getEditorPane().getUndoManager();
420
  }
421
422
  /**
423
   * Forwards the request to the editor pane.
424
   *
425
   * @param <T>      The type of event listener to add.
426
   * @param <U>      The type of consumer to add.
427
   * @param event    The event that should trigger updates to the listener.
428
   * @param consumer The listener to receive update events.
429
   */
430
  public <T extends Event, U extends T> void addEventListener(
431
      final EventPattern<? super T, ? extends U> event,
432
      final Consumer<? super U> consumer ) {
433
    getEditorPane().addKeyboardListener( event, consumer );
434
  }
435
436
  /**
437
   * Forwards to the editor pane's listeners for keyboard events.
438
   *
439
   * @param map The new input map to replace the existing keyboard listener.
440
   */
441
  public void addEventListener( final InputMap<InputEvent> map ) {
442
    getEditorPane().addEventListener( map );
443
  }
444
445
  /**
446
   * Forwards to the editor pane's listeners for keyboard events.
447
   *
448
   * @param map The existing input map to remove from the keyboard listeners.
449
   */
450
  public void removeEventListener( final InputMap<InputEvent> map ) {
451
    getEditorPane().removeEventListener( map );
452
  }
453
454
  /**
455
   * Forwards to the editor pane's listeners for text change events.
456
   *
457
   * @param listener The listener to notify when the text changes.
458
   */
459
  public void addTextChangeListener( final ChangeListener<String> listener ) {
460
    getEditorPane().addTextChangeListener( listener );
461
  }
462
463
  /**
464
   * Forwards to the editor pane's listeners for caret paragraph change events.
465
   *
466
   * @param listener The listener to notify when the caret changes paragraphs.
467
   */
468
  public void addCaretParagraphListener(
469
      final ChangeListener<Integer> listener ) {
470
    getEditorPane().addCaretParagraphListener( listener );
471
  }
472
473
  /**
474
   * Forwards the request to the editor pane.
475
   *
476
   * @return The text to process.
477
   */
478
  public String getEditorText() {
479
    return getEditorPane().getText();
480
  }
481
482
  /**
483
   * Returns the editor pane, or creates one if it doesn't yet exist.
484
   *
485
   * @return The editor pane, never null.
486
   */
487
  public synchronized EditorPane getEditorPane() {
488
    if( this.editorPane == null ) {
489
      this.editorPane = new MarkdownEditorPane();
490
    }
491
492
    return this.editorPane;
493
  }
494
495
  private Notifier getNotifyService() {
496
    return this.alertService;
497
  }
498
499
  /**
500
   * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
501
   * determined.
502
   *
503
   * @return The file encoding or UTF-8 if unknown.
504
   */
505
  private Charset getEncoding() {
506
    if( this.encoding == null ) {
507
      this.encoding = UTF_8;
508
    }
509
510
    return this.encoding;
511
  }
512
513
  private void setEncoding( final Charset encoding ) {
514
    this.encoding = encoding;
515
  }
516
517
  /**
518
   * Returns the tab title, without any modified indicators.
519
   *
520
   * @return The tab title.
521
   */
522
  @Override
523
  public String toString() {
524
    return getTabTitle();
525
  }
526
}
1527
A src/main/java/com/scrivenvar/FileEditorTabPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import 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.beans.value.ObservableValue;
42
import javafx.collections.ListChangeListener;
43
import javafx.collections.ObservableList;
44
import javafx.event.Event;
45
import javafx.scene.Node;
46
import javafx.scene.control.Alert;
47
import javafx.scene.control.ButtonType;
48
import javafx.scene.control.Tab;
49
import javafx.scene.control.TabPane;
50
import javafx.scene.input.InputEvent;
51
import javafx.stage.FileChooser;
52
import javafx.stage.FileChooser.ExtensionFilter;
53
import javafx.stage.Window;
54
import org.fxmisc.richtext.StyledTextArea;
55
import org.fxmisc.wellbehaved.event.EventPattern;
56
import org.fxmisc.wellbehaved.event.InputMap;
57
58
import java.io.File;
59
import java.nio.file.Path;
60
import java.util.ArrayList;
61
import java.util.List;
62
import java.util.function.Consumer;
63
import java.util.prefs.Preferences;
64
import java.util.stream.Collectors;
65
66
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
67
import static com.scrivenvar.FileType.*;
68
import static com.scrivenvar.Messages.get;
69
import static com.scrivenvar.service.events.Notifier.NO;
70
import static com.scrivenvar.service.events.Notifier.YES;
71
72
/**
73
 * Tab pane for file editors.
74
 *
75
 * @author Karl Tauber and White Magic Software, Ltd.
76
 */
77
public final class FileEditorTabPane extends TabPane {
78
79
  private final static String FILTER_EXTENSION_TITLES = "Dialog.file.choose" +
80
      ".filter";
81
82
  private final Options options = Services.load( Options.class );
83
  private final Settings settings = Services.load( Settings.class );
84
  private final Notifier notifyService = Services.load( Notifier.class );
85
86
  private final ReadOnlyObjectWrapper<Path> openDefinition =
87
      new ReadOnlyObjectWrapper<>();
88
  private final ReadOnlyObjectWrapper<FileEditorTab> activeFileEditor =
89
      new ReadOnlyObjectWrapper<>();
90
  private final ReadOnlyBooleanWrapper anyFileEditorModified =
91
      new ReadOnlyBooleanWrapper();
92
93
  /**
94
   * Constructs a new file editor tab pane.
95
   */
96
  public FileEditorTabPane() {
97
    final ObservableList<Tab> tabs = getTabs();
98
99
    setFocusTraversable( false );
100
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
101
102
    addTabSelectionListener(
103
        ( ObservableValue<? extends Tab> tabPane,
104
          final Tab oldTab, final Tab newTab ) -> {
105
106
          if( newTab != null ) {
107
            activeFileEditor.set( (FileEditorTab) newTab );
108
          }
109
        }
110
    );
111
112
    final ChangeListener<Boolean> modifiedListener = ( observable, oldValue,
113
                                                       newValue ) -> {
114
      for( final Tab tab : tabs ) {
115
        if( ((FileEditorTab) tab).isModified() ) {
116
          this.anyFileEditorModified.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 ) -> ((FileEditorTab) tab).modifiedProperty()
128
                                                  .addListener( modifiedListener ) );
129
            }
130
            else if( change.wasRemoved() ) {
131
              change.getRemoved().forEach(
132
                  ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
133
                                                  .removeListener(
134
                                                      modifiedListener ) );
135
            }
136
          }
137
138
          // Changes in the tabs may also change anyFileEditorModified property
139
          // (e.g. closed modified file)
140
          modifiedListener.changed( null, null, null );
141
        }
142
    );
143
  }
144
145
  /**
146
   * Delegates to the active file editor.
147
   *
148
   * @param <T>      Event type.
149
   * @param <U>      Consumer type.
150
   * @param event    Event to pass to the editor.
151
   * @param consumer Consumer to pass to the editor.
152
   */
153
  public <T extends Event, U extends T> void addEventListener(
154
      final EventPattern<? super T, ? extends U> event,
155
      final Consumer<? super U> consumer ) {
156
    getActiveFileEditor().addEventListener( event, consumer );
157
  }
158
159
  /**
160
   * Delegates to the active file editor pane, and, ultimately, to its text
161
   * area.
162
   *
163
   * @param map The map of methods to events.
164
   */
165
  public void addEventListener( final InputMap<InputEvent> map ) {
166
    getActiveFileEditor().addEventListener( map );
167
  }
168
169
  /**
170
   * Remove a keyboard event listener from the active file editor.
171
   *
172
   * @param map The keyboard events to remove.
173
   */
174
  public void removeEventListener( final InputMap<InputEvent> map ) {
175
    getActiveFileEditor().removeEventListener( map );
176
  }
177
178
  /**
179
   * Allows observers to be notified when the current file editor tab changes.
180
   *
181
   * @param listener The listener to notify of tab change events.
182
   */
183
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
184
    // Observe the tab so that when a new tab is opened or selected,
185
    // a notification is kicked off.
186
    getSelectionModel().selectedItemProperty().addListener( listener );
187
  }
188
189
  /**
190
   * Allows clients to manipulate the editor content directly.
191
   *
192
   * @return The text area for the active file editor.
193
   */
194
  public StyledTextArea getEditor() {
195
    return getActiveFileEditor().getEditorPane().getEditor();
196
  }
197
198
  /**
199
   * Returns the tab that has keyboard focus.
200
   *
201
   * @return A non-null instance.
202
   */
203
  public FileEditorTab getActiveFileEditor() {
204
    return this.activeFileEditor.get();
205
  }
206
207
  /**
208
   * Returns the property corresponding to the tab that has focus.
209
   *
210
   * @return A non-null instance.
211
   */
212
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
213
    return this.activeFileEditor.getReadOnlyProperty();
214
  }
215
216
  /**
217
   * Property that can answer whether the text has been modified.
218
   *
219
   * @return A non-null instance, true meaning the content has not been saved.
220
   */
221
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
222
    return this.anyFileEditorModified.getReadOnlyProperty();
223
  }
224
225
  /**
226
   * Creates a new editor instance from the given path.
227
   *
228
   * @param path The file to open.
229
   * @return A non-null instance.
230
   */
231
  private FileEditorTab createFileEditor( final Path path ) {
232
    final FileEditorTab tab = new FileEditorTab( path );
233
234
    tab.setOnCloseRequest( e -> {
235
      if( !canCloseEditor( tab ) ) {
236
        e.consume();
237
      }
238
    } );
239
240
    return tab;
241
  }
242
243
  /**
244
   * Called when the user selects New from the File menu.
245
   */
246
  void newEditor() {
247
    final FileEditorTab tab = createFileEditor( null );
248
249
    getTabs().add( tab );
250
    getSelectionModel().select( tab );
251
  }
252
253
  void openFileDialog() {
254
    final String title = get( "Dialog.file.choose.open.title" );
255
    final FileChooser dialog = createFileChooser( title );
256
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
257
258
    if( files != null ) {
259
      openFiles( files );
260
    }
261
  }
262
263
  /**
264
   * Opens the files into new editors, unless one of those files was a
265
   * definition file. The definition file is loaded into the definition pane,
266
   * but only the first one selected (multiple definition files will result in a
267
   * warning).
268
   *
269
   * @param files The list of non-definition files that the were requested to
270
   *              open.
271
   */
272
  private void openFiles( final List<File> files ) {
273
    final FileTypePredicate predicate
274
        =
275
        new FileTypePredicate( createExtensionFilter( DEFINITION ).getExtensions() );
276
277
    // The user might have opened multiple definitions files. These will
278
    // be discarded from the text editable files.
279
    final List<File> definitions
280
        = files.stream().filter( predicate ).collect( Collectors.toList() );
281
282
    // Create a modifiable list to remove any definition files that were
283
    // opened.
284
    final List<File> editors = new ArrayList<>( files );
285
286
    if( editors.size() > 0 ) {
287
      saveLastDirectory( editors.get( 0 ) );
288
    }
289
290
    editors.removeAll( definitions );
291
292
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
293
    if( editors.size() > 0 ) {
294
      openEditors( editors, 0 );
295
    }
296
297
    if( definitions.size() > 0 ) {
298
      openDefinition( definitions.get( 0 ) );
299
    }
300
  }
301
302
  private void openEditors( final List<File> files, final int activeIndex ) {
303
    final int fileTally = files.size();
304
    final List<Tab> tabs = getTabs();
305
306
    // Close single unmodified "Untitled" tab.
307
    if( tabs.size() == 1 ) {
308
      final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
309
310
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
311
        closeEditor( fileEditor, false );
312
      }
313
    }
314
315
    for( int i = 0; i < fileTally; i++ ) {
316
      final Path path = files.get( i ).toPath();
317
318
      FileEditorTab fileEditorTab = findEditor( path );
319
320
      // Only open new files.
321
      if( fileEditorTab == null ) {
322
        fileEditorTab = createFileEditor( path );
323
        getTabs().add( fileEditorTab );
324
      }
325
326
      // Select the first file in the list.
327
      if( i == activeIndex ) {
328
        getSelectionModel().select( fileEditorTab );
329
      }
330
    }
331
  }
332
333
  /**
334
   * Returns a property that changes when a new definition file is opened.
335
   *
336
   * @return The path to a definition file that was opened.
337
   */
338
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
339
    return getOnOpenDefinitionFile().getReadOnlyProperty();
340
  }
341
342
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
343
    return this.openDefinition;
344
  }
345
346
  /**
347
   * Called when the user has opened a definition file (using the file open
348
   * dialog box). This will replace the current set of definitions for the
349
   * active tab.
350
   *
351
   * @param definition The file to open.
352
   */
353
  private void openDefinition( final File definition ) {
354
    // TODO: Prevent reading this file twice when a new text document is opened.
355
    // (might be a matter of checking the value first).
356
    getOnOpenDefinitionFile().set( definition.toPath() );
357
  }
358
359
  /**
360
   * Called when the contents of the editor are to be saved.
361
   *
362
   * @param tab The tab containing content to save.
363
   * @return true The contents were saved (or needn't be saved).
364
   */
365
  public boolean saveEditor( final FileEditorTab tab ) {
366
    if( tab == null || !tab.isModified() ) {
367
      return true;
368
    }
369
370
    return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
371
  }
372
373
  /**
374
   * Opens the Save As dialog for the user to save the content under a new
375
   * path.
376
   *
377
   * @param tab The tab with contents to save.
378
   * @return true The contents were saved, or the tab was null.
379
   */
380
  public boolean saveEditorAs( final FileEditorTab tab ) {
381
    if( tab == null ) {
382
      return true;
383
    }
384
385
    getSelectionModel().select( tab );
386
387
    final FileChooser fileChooser = createFileChooser( get(
388
        "Dialog.file.choose.save.title" ) );
389
    final File file = fileChooser.showSaveDialog( getWindow() );
390
    if( file == null ) {
391
      return false;
392
    }
393
394
    saveLastDirectory( file );
395
    tab.setPath( file.toPath() );
396
397
    return tab.save();
398
  }
399
400
  void saveAllEditors() {
401
    for( final FileEditorTab fileEditor : getAllEditors() ) {
402
      saveEditor( fileEditor );
403
    }
404
  }
405
406
  /**
407
   * Answers whether the file has had modifications. '
408
   *
409
   * @param tab THe tab to check for modifications.
410
   * @return false The file is unmodified.
411
   */
412
  boolean canCloseEditor( final FileEditorTab tab ) {
413
    if( !tab.isModified() ) {
414
      return true;
415
    }
416
417
    final Notification message = getNotifyService().createNotification(
418
        Messages.get( "Alert.file.close.title" ),
419
        Messages.get( "Alert.file.close.text" ),
420
        tab.getText()
421
    );
422
423
    final Alert alert = getNotifyService().createConfirmation(
424
        getWindow(), message );
425
    final ButtonType response = alert.showAndWait().get();
426
427
    return response == YES ? saveEditor( tab ) : response == NO;
428
  }
429
430
  private Notifier getNotifyService() {
431
    return this.notifyService;
432
  }
433
434
  boolean closeEditor( final FileEditorTab tab, final boolean save ) {
435
    if( tab == null ) {
436
      return true;
437
    }
438
439
    if( save ) {
440
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
441
      Event.fireEvent( tab, event );
442
443
      if( event.isConsumed() ) {
444
        return false;
445
      }
446
    }
447
448
    getTabs().remove( tab );
449
450
    if( tab.getOnClosed() != null ) {
451
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
452
    }
453
454
    return true;
455
  }
456
457
  boolean closeAllEditors() {
458
    final FileEditorTab[] allEditors = getAllEditors();
459
    final FileEditorTab activeEditor = getActiveFileEditor();
460
461
    // try to save active tab first because in case the user decides to cancel,
462
    // then it stays active
463
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
464
      return false;
465
    }
466
467
    // This should be called any time a tab changes.
468
    persistPreferences();
469
470
    // save modified tabs
471
    for( int i = 0; i < allEditors.length; i++ ) {
472
      final FileEditorTab fileEditor = allEditors[ i ];
473
474
      if( fileEditor == activeEditor ) {
475
        continue;
476
      }
477
478
      if( fileEditor.isModified() ) {
479
        // activate the modified tab to make its modified content visible to
480
        // the user
481
        getSelectionModel().select( i );
482
483
        if( !canCloseEditor( fileEditor ) ) {
484
          return false;
485
        }
486
      }
487
    }
488
489
    // Close all tabs.
490
    for( final FileEditorTab fileEditor : allEditors ) {
491
      if( !closeEditor( fileEditor, false ) ) {
492
        return false;
493
      }
494
    }
495
496
    return getTabs().isEmpty();
497
  }
498
499
  private FileEditorTab[] getAllEditors() {
500
    final ObservableList<Tab> tabs = getTabs();
501
    final int length = tabs.size();
502
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
503
504
    for( int i = 0; i < length; i++ ) {
505
      allEditors[ i ] = (FileEditorTab) tabs.get( i );
506
    }
507
508
    return allEditors;
509
  }
510
511
  /**
512
   * Returns the file editor tab that has the given path.
513
   *
514
   * @return null No file editor tab for the given path was found.
515
   */
516
  private FileEditorTab findEditor( final Path path ) {
517
    for( final Tab tab : getTabs() ) {
518
      final FileEditorTab fileEditor = (FileEditorTab) tab;
519
520
      if( fileEditor.isPath( path ) ) {
521
        return fileEditor;
522
      }
523
    }
524
525
    return null;
526
  }
527
528
  private FileChooser createFileChooser( String title ) {
529
    final FileChooser fileChooser = new FileChooser();
530
531
    fileChooser.setTitle( title );
532
    fileChooser.getExtensionFilters().addAll(
533
        createExtensionFilters() );
534
535
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
536
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
537
538
    if( !file.isDirectory() ) {
539
      file = new File( "." );
540
    }
541
542
    fileChooser.setInitialDirectory( file );
543
    return fileChooser;
544
  }
545
546
  private List<ExtensionFilter> createExtensionFilters() {
547
    final List<ExtensionFilter> list = new ArrayList<>();
548
549
    // TODO: Return a list of all properties that match the filter prefix.
550
    // This will allow dynamic filters to be added and removed just by
551
    // updating the properties file.
552
    list.add( createExtensionFilter( SOURCE ) );
553
    list.add( createExtensionFilter( DEFINITION ) );
554
    list.add( createExtensionFilter( XML ) );
555
    list.add( createExtensionFilter( ALL ) );
556
    return list;
557
  }
558
559
  /**
560
   * Returns a filter for file name extensions recognized by the application
561
   * that can be opened by the user.
562
   *
563
   * @param filetype Used to find the globbing pattern for extensions.
564
   * @return A filename filter suitable for use by a FileDialog instance.
565
   */
566
  private ExtensionFilter createExtensionFilter( final FileType filetype ) {
567
    final String tKey = String.format( "%s.title.%s",
568
                                       FILTER_EXTENSION_TITLES,
569
                                       filetype );
570
    final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
571
572
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
573
  }
574
575
  private List<String> getExtensions( final String key ) {
576
    return getSettings().getStringSettingList( key );
577
  }
578
579
  private void saveLastDirectory( final File file ) {
580
    getPreferences().put( "lastDirectory", file.getParent() );
581
  }
582
583
  public void restorePreferences() {
584
    int activeIndex = 0;
585
586
    final Preferences preferences = getPreferences();
587
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
588
    final String activeFileName = preferences.get( "activeFile", null );
589
590
    final ArrayList<File> files = new ArrayList<>( fileNames.length );
591
592
    for( final String fileName : fileNames ) {
593
      final File file = new File( fileName );
594
595
      if( file.exists() ) {
596
        files.add( file );
597
598
        if( fileName.equals( activeFileName ) ) {
599
          activeIndex = files.size() - 1;
600
        }
601
      }
602
    }
603
604
    if( files.isEmpty() ) {
605
      newEditor();
606
    }
607
    else {
608
      openEditors( files, activeIndex );
609
    }
610
  }
611
612
  public void persistPreferences() {
613
    final ObservableList<Tab> allEditors = getTabs();
614
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
615
616
    for( final Tab tab : allEditors ) {
617
      final FileEditorTab fileEditor = (FileEditorTab) tab;
618
      final Path filePath = fileEditor.getPath();
619
620
      if( filePath != null ) {
621
        fileNames.add( filePath.toString() );
622
      }
623
    }
624
625
    final Preferences preferences = getPreferences();
626
    Utils.putPrefsStrings( preferences,
627
                           "file",
628
                           fileNames.toArray( new String[ 0 ] ) );
629
630
    final FileEditorTab activeEditor = getActiveFileEditor();
631
    final Path filePath = activeEditor == null ? null : activeEditor.getPath();
632
633
    if( filePath == null ) {
634
      preferences.remove( "activeFile" );
635
    }
636
    else {
637
      preferences.put( "activeFile", filePath.toString() );
638
    }
639
  }
640
641
  private Settings getSettings() {
642
    return this.settings;
643
  }
644
645
  protected Options getOptions() {
646
    return this.options;
647
  }
648
649
  private Window getWindow() {
650
    return getScene().getWindow();
651
  }
652
653
  private Preferences getPreferences() {
654
    return getOptions().getState();
655
  }
656
657
  Node getNode() {
658
    return this;
659
  }
660
}
1661
A src/main/java/com/scrivenvar/FileType.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
/**
31
 * Represents different file type classifications. These are high-level mappings
32
 * that correspond to the list of glob patterns found within
33
 * settings.properties.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public enum FileType {
38
39
  ALL( "all" ),
40
  RMARKDOWN( "rmarkdown" ),
41
  RXML( "rxml" ),
42
  SOURCE( "source" ),
43
  DEFINITION( "definition" ),
44
  XML( "xml" ),
45
  CSV( "csv" ),
46
  JSON( "json" ),
47
  TOML( "toml" ),
48
  YAML( "yaml" ),
49
  PROPERTIES( "properties" );
50
51
  private final String mType;
52
53
  /**
54
   * Default constructor for enumerated file type.
55
   *
56
   * @param type Human-readable name for the file type.
57
   */
58
  FileType( final String type ) {
59
    mType = type;
60
  }
61
62
  /**
63
   * Returns the file type that corresponds to the given string.
64
   *
65
   * @param type The string to compare against this enumeration of file types.
66
   *
67
   * @return The corresponding File Type for the given string.
68
   *
69
   * @throws IllegalArgumentException Type not found.
70
   */
71
  public static FileType from( final String type ) {
72
    for( final FileType fileType : FileType.values() ) {
73
      if( fileType.isType( type ) ) {
74
        return fileType;
75
      }
76
    }
77
78
    throw new IllegalArgumentException( type );
79
  }
80
81
  /**
82
   * Answers whether this file type matches the given string, case insensitive
83
   * comparison.
84
   *
85
   * @param type Presumably a file name extension to check against.
86
   *
87
   * @return true The given extension corresponds to this enumerated type.
88
   */
89
  public boolean isType( final String type ) {
90
    return getType().equalsIgnoreCase( type );
91
  }
92
93
  /**
94
   * Returns the human-readable name for the file type.
95
   *
96
   * @return A non-null instance.
97
   */
98
  private String getType() {
99
    return mType;
100
  }
101
102
  /**
103
   * Returns the lowercase version of the file name extension.
104
   *
105
   * @return The file name, in lower case.
106
   */
107
  @Override
108
  public String toString() {
109
    return getType();
110
  }
111
}
1112
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
/**
31
 * Launches the application using the {@link Main} class.
32
 *
33
 * <p>
34
 * This is required until modules are implemented, which may never happen
35
 * because the application should be ported away from Java and JavaFX.
36
 * </p>
37
 */
38
public class Launcher {
39
  public static void main( final String[] args ) {
40
    Main.main( args );
41
  }
42
}
143
A src/main/java/com/scrivenvar/Main.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import 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.StageState;
35
import javafx.application.Application;
36
import javafx.scene.Scene;
37
import javafx.scene.image.Image;
38
import javafx.stage.Stage;
39
40
import java.util.logging.LogManager;
41
42
import static com.scrivenvar.Constants.*;
43
import static com.scrivenvar.Messages.get;
44
45
/**
46
 * Application entry point. The application allows users to edit Markdown
47
 * files and see a real-time preview of the edits.
48
 *
49
 * @author Karl Tauber and White Magic Software, Ltd.
50
 */
51
public final class Main extends Application {
52
53
  // Suppress logging errors to standard output.
54
  static {
55
    LogManager.getLogManager().reset();
56
  }
57
58
  private static Application sApplication;
59
60
  private final Options mOptions = Services.load( Options.class );
61
  private final Notifier mNotifier = Services.load( Notifier.class );
62
  private final Snitch mSnitch = Services.load( Snitch.class );
63
  private final Thread mSnitchThread = new Thread( getSnitch() );
64
  private final MainWindow mMainWindow = new MainWindow();
65
66
  private StageState mStageState;
67
68
  public static void main( final String[] args ) {
69
    initPreferences();
70
    launch( args );
71
  }
72
73
  /**
74
   * Sets the factory used for reading user preferences.
75
   */
76
  private static void initPreferences() {
77
    System.setProperty(
78
        "java.util.prefs.PreferencesFactory",
79
        FilePreferencesFactory.class.getName()
80
    );
81
  }
82
83
  /**
84
   * Application entry point.
85
   *
86
   * @param stage The primary application stage.
87
   */
88
  @Override
89
  public void start( final Stage stage ) {
90
    initApplication();
91
    initNotifyService();
92
    initState( stage );
93
    initStage( stage );
94
    initSnitch();
95
96
    stage.show();
97
  }
98
99
  public static void showDocument( final String uri ) {
100
    getApplication().getHostServices().showDocument( uri );
101
  }
102
103
  private void initApplication() {
104
    sApplication = this;
105
  }
106
107
  /**
108
   * Constructs the notify service and appends the main window to the list of
109
   * notification observers.
110
   */
111
  private void initNotifyService() {
112
    mNotifier.addObserver( getMainWindow() );
113
  }
114
115
  private void initState( final Stage stage ) {
116
    mStageState = new StageState( stage, getOptions().getState() );
117
  }
118
119
  private void initStage( final Stage stage ) {
120
    stage.getIcons().addAll(
121
        createImage( FILE_LOGO_16 ),
122
        createImage( FILE_LOGO_32 ),
123
        createImage( FILE_LOGO_128 ),
124
        createImage( FILE_LOGO_256 ),
125
        createImage( FILE_LOGO_512 ) );
126
    stage.setTitle( getApplicationTitle() );
127
    stage.setScene( getScene() );
128
  }
129
130
  /**
131
   * Watch for file system changes.
132
   */
133
  private void initSnitch() {
134
    getSnitchThread().start();
135
  }
136
137
  /**
138
   * Stops the snitch service, if its running.
139
   *
140
   * @throws InterruptedException Couldn't stop the snitch thread.
141
   */
142
  @Override
143
  public void stop() throws InterruptedException {
144
    getSnitch().stop();
145
146
    final Thread thread = getSnitchThread();
147
    thread.interrupt();
148
    thread.join();
149
  }
150
151
  private synchronized Snitch getSnitch() {
152
    return mSnitch;
153
  }
154
155
  private Thread getSnitchThread() {
156
    return mSnitchThread;
157
  }
158
159
  private synchronized Options getOptions() {
160
    return mOptions;
161
  }
162
163
  private Scene getScene() {
164
    return getMainWindow().getScene();
165
  }
166
167
  private MainWindow getMainWindow() {
168
    return mMainWindow;
169
  }
170
171
  private static Application getApplication() {
172
    return sApplication;
173
  }
174
175
  private String getApplicationTitle() {
176
    return get( "Main.title" );
177
  }
178
179
  private Image createImage( final String filename ) {
180
    return new Image( filename );
181
  }
182
}
1183
A src/main/java/com/scrivenvar/MainWindow.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.definition.*;
31
import com.scrivenvar.dialogs.RScriptDialog;
32
import com.scrivenvar.editors.EditorPane;
33
import com.scrivenvar.editors.VariableNameInjector;
34
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
35
import com.scrivenvar.predicates.files.FileTypePredicate;
36
import com.scrivenvar.preview.HTMLPreviewPane;
37
import com.scrivenvar.processors.Processor;
38
import com.scrivenvar.processors.ProcessorFactory;
39
import com.scrivenvar.service.Options;
40
import com.scrivenvar.service.Snitch;
41
import com.scrivenvar.service.events.Notifier;
42
import com.scrivenvar.util.Action;
43
import com.scrivenvar.util.ActionUtils;
44
import javafx.application.Platform;
45
import javafx.beans.binding.Bindings;
46
import javafx.beans.binding.BooleanBinding;
47
import javafx.beans.property.BooleanProperty;
48
import javafx.beans.property.SimpleBooleanProperty;
49
import javafx.beans.value.ObservableBooleanValue;
50
import javafx.beans.value.ObservableValue;
51
import javafx.collections.ListChangeListener.Change;
52
import javafx.collections.ObservableList;
53
import javafx.geometry.Pos;
54
import javafx.scene.Node;
55
import javafx.scene.Scene;
56
import javafx.scene.control.*;
57
import javafx.scene.control.Alert.AlertType;
58
import javafx.scene.image.Image;
59
import javafx.scene.image.ImageView;
60
import javafx.scene.input.KeyEvent;
61
import javafx.scene.layout.BorderPane;
62
import javafx.scene.layout.VBox;
63
import javafx.scene.text.Text;
64
import javafx.stage.Window;
65
import javafx.stage.WindowEvent;
66
import org.controlsfx.control.StatusBar;
67
import org.fxmisc.richtext.model.TwoDimensional.Position;
68
69
import java.io.IOException;
70
import java.nio.file.Path;
71
import java.util.*;
72
import java.util.function.Function;
73
import java.util.prefs.Preferences;
74
75
import static com.scrivenvar.Constants.*;
76
import static com.scrivenvar.Messages.get;
77
import static com.scrivenvar.Messages.getLiteral;
78
import static com.scrivenvar.util.StageState.*;
79
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
80
import static javafx.event.Event.fireEvent;
81
import static javafx.scene.input.KeyCode.ESCAPE;
82
import static javafx.scene.input.KeyEvent.CHAR_UNDEFINED;
83
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
84
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
85
86
/**
87
 * Main window containing a tab pane in the center for file editors.
88
 *
89
 * @author Karl Tauber and White Magic Software, Ltd.
90
 */
91
public class MainWindow implements Observer {
92
93
  private final Options mOptions = Services.load( Options.class );
94
  private final Snitch mSnitch = Services.load( Snitch.class );
95
  private final Notifier mNotifier = Services.load( Notifier.class );
96
97
  private Scene scene;
98
  private MenuBar menuBar;
99
  private StatusBar statusBar;
100
  private Text lineNumberText;
101
  private TextField findTextField;
102
103
  private DefinitionSource definitionSource;
104
  private DefinitionPane definitionPane;
105
  private FileEditorTabPane fileEditorPane;
106
  private HTMLPreviewPane previewPane;
107
108
  /**
109
   * Prevents re-instantiation of processing classes.
110
   */
111
  private Map<FileEditorTab, Processor<String>> processors;
112
113
  /**
114
   * Listens on the definition pane for double-click events.
115
   */
116
  private VariableNameInjector variableNameInjector;
117
118
  public MainWindow() {
119
    initLayout();
120
    initFindInput();
121
    initSnitch();
122
    initDefinitionListener();
123
    initTabAddedListener();
124
    initTabChangedListener();
125
    initPreferences();
126
  }
127
128
  /**
129
   * Watch for changes to external files. In particular, this awaits
130
   * modifications to any XSL files associated with XML files being edited. When
131
   * an XSL file is modified (external to the application), the snitch's ears
132
   * perk up and the file is reloaded. This keeps the XSL transformation up to
133
   * date with what's on the file system.
134
   */
135
  private void initSnitch() {
136
    getSnitch().addObserver( this );
137
  }
138
139
  /**
140
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
141
   * presses.
142
   */
143
  private void initFindInput() {
144
    final TextField input = getFindTextField();
145
146
    input.setOnKeyPressed( ( KeyEvent event ) -> {
147
      switch( event.getCode() ) {
148
        case F3:
149
        case ENTER:
150
          findNext();
151
          break;
152
        case F:
153
          if( !event.isControlDown() ) {
154
            break;
155
          }
156
        case ESCAPE:
157
          getStatusBar().setGraphic( null );
158
          getActiveFileEditor().getEditorPane().requestFocus();
159
          break;
160
      }
161
    } );
162
163
    // Remove when the input field loses focus.
164
    input.focusedProperty().addListener(
165
        (
166
            final ObservableValue<? extends Boolean> focused,
167
            final Boolean oFocus,
168
            final Boolean nFocus ) -> {
169
          if( !nFocus ) {
170
            getStatusBar().setGraphic( null );
171
          }
172
        }
173
    );
174
  }
175
176
  /**
177
   * Listen for file editor tab pane to receive an open definition source event.
178
   */
179
  private void initDefinitionListener() {
180
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
181
        ( ObservableValue<? extends Path> definitionFile,
182
          final Path oldPath, final Path newPath ) -> {
183
          openDefinition( newPath );
184
185
          // Indirectly refresh the resolved map.
186
          setProcessors( null );
187
          updateDefinitionPane();
188
189
          try {
190
            getSnitch().ignore( oldPath );
191
            getSnitch().listen( newPath );
192
          } catch( final IOException ex ) {
193
            error( ex );
194
          }
195
196
          // Will create new processors and therefore a new resolved map.
197
          refreshSelectedTab( getActiveFileEditor() );
198
        }
199
    );
200
  }
201
202
  /**
203
   * When tabs are added, hook the various change listeners onto the new tab so
204
   * that the preview pane refreshes as necessary.
205
   */
206
  private void initTabAddedListener() {
207
    final FileEditorTabPane editorPane = getFileEditorPane();
208
209
    // Make sure the text processor kicks off when new files are opened.
210
    final ObservableList<Tab> tabs = editorPane.getTabs();
211
212
    // Update the preview pane on tab changes.
213
    tabs.addListener(
214
        ( final Change<? extends Tab> change ) -> {
215
          while( change.next() ) {
216
            if( change.wasAdded() ) {
217
              // Multiple tabs can be added simultaneously.
218
              for( final Tab newTab : change.getAddedSubList() ) {
219
                final FileEditorTab tab = (FileEditorTab) newTab;
220
221
                initTextChangeListener( tab );
222
                initCaretParagraphListener( tab );
223
                initKeyboardEventListeners( tab );
224
//              initSyntaxListener( tab );
225
              }
226
            }
227
          }
228
        }
229
    );
230
  }
231
232
  /**
233
   * Reloads the preferences from the previous session.
234
   */
235
  private void initPreferences() {
236
    restoreDefinitionSource();
237
    getFileEditorPane().restorePreferences();
238
    updateDefinitionPane();
239
  }
240
241
  /**
242
   * Listen for new tab selection events.
243
   */
244
  private void initTabChangedListener() {
245
    final FileEditorTabPane editorPane = getFileEditorPane();
246
247
    // Update the preview pane changing tabs.
248
    editorPane.addTabSelectionListener(
249
        ( ObservableValue<? extends Tab> tabPane,
250
          final Tab oldTab, final Tab newTab ) -> {
251
          updateVariableNameInjector();
252
253
          // If there was no old tab, then this is a first time load, which
254
          // can be ignored.
255
          if( oldTab != null ) {
256
            if( newTab == null ) {
257
              closeRemainingTab();
258
            }
259
            else {
260
              // Update the preview with the edited text.
261
              refreshSelectedTab( (FileEditorTab) newTab );
262
            }
263
          }
264
        }
265
    );
266
  }
267
268
  /**
269
   * Ensure that the keyboard events are received when a new tab is added
270
   * to the user interface.
271
   *
272
   * @param tab The tab that can trigger keyboard events, such as control+space.
273
   */
274
  private void initKeyboardEventListeners( final FileEditorTab tab ) {
275
    final VariableNameInjector vin = getVariableNameInjector();
276
    vin.initKeyboardEventListeners( tab );
277
  }
278
279
  private void initTextChangeListener( final FileEditorTab tab ) {
280
    tab.addTextChangeListener(
281
        ( ObservableValue<? extends String> editor,
282
          final String oldValue, final String newValue ) ->
283
            refreshSelectedTab( tab )
284
    );
285
  }
286
287
  private void initCaretParagraphListener( final FileEditorTab tab ) {
288
    tab.addCaretParagraphListener(
289
        ( ObservableValue<? extends Integer> editor,
290
          final Integer oldValue, final Integer newValue ) ->
291
            refreshSelectedTab( tab )
292
    );
293
  }
294
295
  private void updateVariableNameInjector() {
296
    getVariableNameInjector().setFileEditorTab( getActiveFileEditor() );
297
  }
298
299
  private void setVariableNameInjector( final VariableNameInjector injector ) {
300
    this.variableNameInjector = injector;
301
  }
302
303
  private synchronized VariableNameInjector getVariableNameInjector() {
304
    if( this.variableNameInjector == null ) {
305
      final VariableNameInjector vin = createVariableNameInjector();
306
      setVariableNameInjector( vin );
307
    }
308
309
    return this.variableNameInjector;
310
  }
311
312
  private VariableNameInjector createVariableNameInjector() {
313
    final FileEditorTab tab = getActiveFileEditor();
314
    final DefinitionPane pane = getDefinitionPane();
315
316
    return new VariableNameInjector( tab, pane );
317
  }
318
319
  /**
320
   * Add a listener for variable name injection the given tab.
321
   *
322
   * @param tab The tab to inject variable names into upon a double-click.
323
   */
324
  private void initVariableNameInjector( final Tab tab ) {
325
    final FileEditorTab editorTab = (FileEditorTab) tab;
326
  }
327
328
  /**
329
   * Called whenever the preview pane becomes out of sync with the file editor
330
   * tab. This can be called when the text changes, the caret paragraph changes,
331
   * or the file tab changes.
332
   *
333
   * @param tab The file editor tab that has been changed in some fashion.
334
   */
335
  private void refreshSelectedTab( final FileEditorTab tab ) {
336
    if( tab.isFileOpen() ) {
337
      getPreviewPane().setPath( tab.getPath() );
338
339
      // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
340
      final Position p = tab.getCaretOffset();
341
      getLineNumberText().setText(
342
          get( STATUS_BAR_LINE,
343
               p.getMajor() + 1,
344
               p.getMinor() + 1,
345
               tab.getCaretPosition() + 1
346
          )
347
      );
348
349
      Processor<String> processor = getProcessors().get( tab );
350
351
      if( processor == null ) {
352
        processor = createProcessor( tab );
353
        getProcessors().put( tab, processor );
354
      }
355
356
      try {
357
        getNotifier().clear();
358
        processor.processChain( tab.getEditorText() );
359
      } catch( final Exception ex ) {
360
        error( ex );
361
      }
362
    }
363
  }
364
365
  /**
366
   * Used to find text in the active file editor window.
367
   */
368
  private void find() {
369
    final TextField input = getFindTextField();
370
    getStatusBar().setGraphic( input );
371
    input.requestFocus();
372
  }
373
374
  public void findNext() {
375
    getActiveFileEditor().searchNext( getFindTextField().getText() );
376
  }
377
378
  /**
379
   * Returns the variable map of interpolated definitions.
380
   *
381
   * @return A map to help dereference variables.
382
   */
383
  private Map<String, String> getResolvedMap() {
384
    return getDefinitionSource().getResolvedMap();
385
  }
386
387
  /**
388
   * Returns the root node for the hierarchical definition source.
389
   *
390
   * @return Data to display in the definition pane.
391
   */
392
  private TreeView<String> getTreeView() {
393
    try {
394
      return getDefinitionSource().asTreeView();
395
    } catch( Exception e ) {
396
      error( e );
397
    }
398
399
    // Slightly redundant as getDefinitionSource() might have returned an
400
    // empty definition source.
401
    return (new EmptyDefinitionSource()).asTreeView();
402
  }
403
404
  /**
405
   * Called when a definition source is opened.
406
   *
407
   * @param path Path to the definition source that was opened.
408
   */
409
  private void openDefinition( final Path path ) {
410
    try {
411
      final DefinitionSource ds = createDefinitionSource( path.toString() );
412
      setDefinitionSource( ds );
413
      storeDefinitionSource();
414
      updateDefinitionPane();
415
    } catch( final Exception e ) {
416
      error( e );
417
    }
418
  }
419
420
  private void updateDefinitionPane() {
421
    getDefinitionPane().setRoot( getDefinitionSource().asTreeView() );
422
  }
423
424
  private void restoreDefinitionSource() {
425
    final Preferences preferences = getPreferences();
426
    final String source = preferences.get( PERSIST_DEFINITION_SOURCE, "" );
427
428
    setDefinitionSource( createDefinitionSource( source ) );
429
  }
430
431
  private void storeDefinitionSource() {
432
    final Preferences preferences = getPreferences();
433
    final DefinitionSource ds = getDefinitionSource();
434
435
    preferences.put( PERSIST_DEFINITION_SOURCE, ds.toString() );
436
  }
437
438
  /**
439
   * Called when the last open tab is closed to clear the preview pane.
440
   */
441
  private void closeRemainingTab() {
442
    getPreviewPane().clear();
443
  }
444
445
  /**
446
   * Called when an exception occurs that warrants the user's attention.
447
   *
448
   * @param e The exception with a message that the user should know about.
449
   */
450
  private void error( final Exception e ) {
451
    getNotifier().notify( e );
452
  }
453
454
  //---- File actions -------------------------------------------------------
455
456
  /**
457
   * Called when an observable instance has changed. This is called by both the
458
   * snitch service and the notify service. The snitch service can be called for
459
   * different file types, including definition sources.
460
   *
461
   * @param observable The observed instance.
462
   * @param value      The noteworthy item.
463
   */
464
  @Override
465
  public void update( final Observable observable, final Object value ) {
466
    if( value != null ) {
467
      if( observable instanceof Snitch && value instanceof Path ) {
468
        final Path path = (Path) value;
469
        final FileTypePredicate predicate
470
            = new FileTypePredicate( GLOB_DEFINITION_EXTENSIONS );
471
472
        // Reload definitions.
473
        if( predicate.test( path.toFile() ) ) {
474
          updateDefinitionSource( path );
475
        }
476
477
        updateSelectedTab();
478
      }
479
      else if( observable instanceof Notifier && value instanceof String ) {
480
        updateStatusBar( (String) value );
481
      }
482
    }
483
  }
484
485
  /**
486
   * Updates the status bar to show the given message.
487
   *
488
   * @param s The message to show in the status bar.
489
   */
490
  private void updateStatusBar( final String s ) {
491
    Platform.runLater(
492
        () -> {
493
          final int index = s.indexOf( '\n' );
494
          final String message = s.substring( 0,
495
                                              index > 0 ? index : s.length() );
496
497
          getStatusBar().setText( message );
498
        }
499
    );
500
  }
501
502
  /**
503
   * Called when a file has been modified.
504
   */
505
  private void updateSelectedTab() {
506
    Platform.runLater(
507
        () -> {
508
          // Brute-force XSLT file reload by re-instantiating all processors.
509
          resetProcessors();
510
          refreshSelectedTab( getActiveFileEditor() );
511
        }
512
    );
513
  }
514
515
  /**
516
   * Reloads the definition source from the given path.
517
   *
518
   * @param path The path containing new definition information.
519
   */
520
  private void updateDefinitionSource( final Path path ) {
521
    Platform.runLater( () -> openDefinition( path ) );
522
  }
523
524
  /**
525
   * After resetting the processors, they will refresh anew to be up-to-date
526
   * with the files (text and definition) currently loaded into the editor.
527
   */
528
  private void resetProcessors() {
529
    getProcessors().clear();
530
  }
531
532
  //---- File actions -------------------------------------------------------
533
  private void fileNew() {
534
    getFileEditorPane().newEditor();
535
  }
536
537
  private void fileOpen() {
538
    getFileEditorPane().openFileDialog();
539
  }
540
541
  private void fileClose() {
542
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
543
  }
544
545
  private void fileCloseAll() {
546
    getFileEditorPane().closeAllEditors();
547
  }
548
549
  private void fileSave() {
550
    getFileEditorPane().saveEditor( getActiveFileEditor() );
551
  }
552
553
  private void fileSaveAs() {
554
    final FileEditorTab editor = getActiveFileEditor();
555
    getFileEditorPane().saveEditorAs( editor );
556
    getProcessors().remove( editor );
557
558
    try {
559
      refreshSelectedTab( editor );
560
    } catch( final Exception ex ) {
561
      getNotifier().notify( ex );
562
    }
563
  }
564
565
  private void fileSaveAll() {
566
    getFileEditorPane().saveAllEditors();
567
  }
568
569
  private void fileExit() {
570
    final Window window = getWindow();
571
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
572
  }
573
574
  //---- R menu actions
575
  private void rScript() {
576
    final String script = getPreferences().get( PERSIST_R_STARTUP, "" );
577
    final RScriptDialog dialog = new RScriptDialog(
578
        getWindow(), "Dialog.r.script.title", script );
579
    final Optional<String> result = dialog.showAndWait();
580
581
    result.ifPresent( this::putStartupScript );
582
  }
583
584
  private void rDirectory() {
585
    final TextInputDialog dialog = new TextInputDialog(
586
        getPreferences().get( PERSIST_R_DIRECTORY, USER_DIRECTORY )
587
    );
588
589
    dialog.setTitle( get( "Dialog.r.directory.title" ) );
590
    dialog.setHeaderText( getLiteral( "Dialog.r.directory.header" ) );
591
    dialog.setContentText( "Directory" );
592
593
    final Optional<String> result = dialog.showAndWait();
594
595
    result.ifPresent( this::putStartupDirectory );
596
  }
597
598
  /**
599
   * Stores the R startup script into the user preferences.
600
   */
601
  private void putStartupScript( final String script ) {
602
    putPreference( PERSIST_R_STARTUP, script );
603
  }
604
605
  /**
606
   * Stores the R bootstrap script directory into the user preferences.
607
   */
608
  private void putStartupDirectory( final String directory ) {
609
    putPreference( PERSIST_R_DIRECTORY, directory );
610
  }
611
612
  //---- Help actions -------------------------------------------------------
613
  private void helpAbout() {
614
    Alert alert = new Alert( AlertType.INFORMATION );
615
    alert.setTitle( get( "Dialog.about.title" ) );
616
    alert.setHeaderText( get( "Dialog.about.header" ) );
617
    alert.setContentText( get( "Dialog.about.content" ) );
618
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
619
    alert.initOwner( getWindow() );
620
621
    alert.showAndWait();
622
  }
623
624
  //---- Convenience accessors ----------------------------------------------
625
  private float getFloat( final String key, final float defaultValue ) {
626
    return getPreferences().getFloat( key, defaultValue );
627
  }
628
629
  private Preferences getPreferences() {
630
    return getOptions().getState();
631
  }
632
633
  protected Scene getScene() {
634
    if( this.scene == null ) {
635
      this.scene = createScene();
636
    }
637
638
    return this.scene;
639
  }
640
641
  public Window getWindow() {
642
    return getScene().getWindow();
643
  }
644
645
  private MarkdownEditorPane getActiveEditor() {
646
    final EditorPane pane = getActiveFileEditor().getEditorPane();
647
648
    return pane instanceof MarkdownEditorPane ? (MarkdownEditorPane) pane :
649
        null;
650
  }
651
652
  private FileEditorTab getActiveFileEditor() {
653
    return getFileEditorPane().getActiveFileEditor();
654
  }
655
656
  //---- Member accessors ---------------------------------------------------
657
  private void setProcessors(
658
      final Map<FileEditorTab, Processor<String>> map ) {
659
    this.processors = map;
660
  }
661
662
  private Map<FileEditorTab, Processor<String>> getProcessors() {
663
    if( this.processors == null ) {
664
      setProcessors( new HashMap<>() );
665
    }
666
667
    return this.processors;
668
  }
669
670
  private FileEditorTabPane getFileEditorPane() {
671
    if( this.fileEditorPane == null ) {
672
      this.fileEditorPane = createFileEditorPane();
673
    }
674
675
    return this.fileEditorPane;
676
  }
677
678
  private HTMLPreviewPane getPreviewPane() {
679
    if( this.previewPane == null ) {
680
      this.previewPane = createPreviewPane();
681
    }
682
683
    return this.previewPane;
684
  }
685
686
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
687
    this.definitionSource = definitionSource;
688
  }
689
690
  private DefinitionSource getDefinitionSource() {
691
    if( this.definitionSource == null ) {
692
      this.definitionSource = new EmptyDefinitionSource();
693
    }
694
695
    return this.definitionSource;
696
  }
697
698
  private DefinitionPane getDefinitionPane() {
699
    if( this.definitionPane == null ) {
700
      this.definitionPane = createDefinitionPane();
701
    }
702
703
    return this.definitionPane;
704
  }
705
706
  private Options getOptions() {
707
    return mOptions;
708
  }
709
710
  private Snitch getSnitch() {
711
    return mSnitch;
712
  }
713
714
  private Notifier getNotifier() {
715
    return mNotifier;
716
  }
717
718
  public void setMenuBar( final MenuBar menuBar ) {
719
    this.menuBar = menuBar;
720
  }
721
722
  public MenuBar getMenuBar() {
723
    return this.menuBar;
724
  }
725
726
  private Text getLineNumberText() {
727
    if( this.lineNumberText == null ) {
728
      this.lineNumberText = createLineNumberText();
729
    }
730
731
    return this.lineNumberText;
732
  }
733
734
  private synchronized StatusBar getStatusBar() {
735
    if( this.statusBar == null ) {
736
      this.statusBar = createStatusBar();
737
    }
738
739
    return this.statusBar;
740
  }
741
742
  private TextField getFindTextField() {
743
    if( this.findTextField == null ) {
744
      this.findTextField = createFindTextField();
745
    }
746
747
    return this.findTextField;
748
  }
749
750
  //---- Member creators ----------------------------------------------------
751
752
  /**
753
   * Factory to create processors that are suited to different file types.
754
   *
755
   * @param tab The tab that is subjected to processing.
756
   * @return A processor suited to the file type specified by the tab's path.
757
   */
758
  private Processor<String> createProcessor( final FileEditorTab tab ) {
759
    return createProcessorFactory().createProcessor( tab );
760
  }
761
762
  private ProcessorFactory createProcessorFactory() {
763
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
764
  }
765
766
  private DefinitionSource createDefinitionSource( final String path ) {
767
    DefinitionSource ds;
768
769
    try {
770
      ds = createDefinitionFactory().createDefinitionSource( path );
771
772
      if( ds instanceof FileDefinitionSource ) {
773
        try {
774
          getNotifier().notify( ds.getError() );
775
          getSnitch().listen( ((FileDefinitionSource) ds).getPath() );
776
        } catch( final Exception ex ) {
777
          error( ex );
778
        }
779
      }
780
    } catch( final Exception ex ) {
781
      ds = new EmptyDefinitionSource();
782
      error( ex );
783
    }
784
785
    return ds;
786
  }
787
788
  private TextField createFindTextField() {
789
    return new TextField();
790
  }
791
792
  /**
793
   * Create an editor pane to hold file editor tabs.
794
   *
795
   * @return A new instance, never null.
796
   */
797
  private FileEditorTabPane createFileEditorPane() {
798
    return new FileEditorTabPane();
799
  }
800
801
  private HTMLPreviewPane createPreviewPane() {
802
    return new HTMLPreviewPane();
803
  }
804
805
  private DefinitionPane createDefinitionPane() {
806
    return new DefinitionPane( getTreeView() );
807
  }
808
809
  private DefinitionFactory createDefinitionFactory() {
810
    return new DefinitionFactory();
811
  }
812
813
  private StatusBar createStatusBar() {
814
    return new StatusBar();
815
  }
816
817
  private Scene createScene() {
818
    final SplitPane splitPane = new SplitPane(
819
        getDefinitionPane().getNode(),
820
        getFileEditorPane().getNode(),
821
        getPreviewPane().getNode() );
822
823
    splitPane.setDividerPositions(
824
        getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
825
        getFloat( K_PANE_SPLIT_EDITOR, .45f ),
826
        getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
827
828
    // See: http://broadlyapplicable.blogspot
829
    // .ca/2015/03/javafx-capture-restorePreferences-splitpane.html
830
    final BorderPane borderPane = new BorderPane();
831
    borderPane.setPrefSize( 1024, 800 );
832
    borderPane.setTop( createMenuBar() );
833
    borderPane.setBottom( getStatusBar() );
834
    borderPane.setCenter( splitPane );
835
836
    final VBox box = new VBox();
837
    box.setAlignment( Pos.BASELINE_CENTER );
838
    box.getChildren().add( getLineNumberText() );
839
    getStatusBar().getRightItems().add( box );
840
841
    return new Scene( borderPane );
842
  }
843
844
  private Text createLineNumberText() {
845
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
846
  }
847
848
  private Node createMenuBar() {
849
    final BooleanBinding activeFileEditorIsNull =
850
        getFileEditorPane().activeFileEditorProperty()
851
                           .isNull();
852
853
    // File actions
854
    final Action fileNewAction = new Action( get( "Main.menu.file.new" ),
855
                                             "Shortcut+N", FILE_ALT,
856
                                             e -> fileNew() );
857
    final Action fileOpenAction = new Action( get( "Main.menu.file.open" ),
858
                                              "Shortcut+O", FOLDER_OPEN_ALT,
859
                                              e -> fileOpen() );
860
    final Action fileCloseAction = new Action( get( "Main.menu.file.close" ),
861
                                               "Shortcut+W", null,
862
                                               e -> fileClose(),
863
                                               activeFileEditorIsNull );
864
    final Action fileCloseAllAction = new Action( get(
865
        "Main.menu.file.close_all" ), null, null, e -> fileCloseAll(),
866
                                                  activeFileEditorIsNull );
867
    final Action fileSaveAction = new Action( get( "Main.menu.file.save" ),
868
                                              "Shortcut+S", FLOPPY_ALT,
869
                                              e -> fileSave(),
870
                                              createActiveBooleanProperty(
871
                                                  FileEditorTab::modifiedProperty )
872
                                                  .not() );
873
    final Action fileSaveAsAction = new Action( Messages.get(
874
        "Main.menu.file.save_as" ), null, null, e -> fileSaveAs(),
875
                                                activeFileEditorIsNull );
876
    final Action fileSaveAllAction = new Action(
877
        get( "Main.menu.file.save_all" ), "Shortcut+Shift+S", null,
878
        e -> fileSaveAll(),
879
        Bindings.not( getFileEditorPane().anyFileEditorModifiedProperty() ) );
880
    final Action fileExitAction = new Action( get( "Main.menu.file.exit" ),
881
                                              null,
882
                                              null,
883
                                              e -> fileExit() );
884
885
    // Edit actions
886
    final Action editUndoAction = new Action( get( "Main.menu.edit.undo" ),
887
                                              "Shortcut+Z", UNDO,
888
                                              e -> getActiveEditor().undo(),
889
                                              createActiveBooleanProperty(
890
                                                  FileEditorTab::canUndoProperty )
891
                                                  .not() );
892
    final Action editRedoAction = new Action( get( "Main.menu.edit.redo" ),
893
                                              "Shortcut+Y", REPEAT,
894
                                              e -> getActiveEditor().redo(),
895
                                              createActiveBooleanProperty(
896
                                                  FileEditorTab::canRedoProperty )
897
                                                  .not() );
898
    final Action editFindAction = new Action( Messages.get(
899
        "Main.menu.edit.find" ), "Ctrl+F", SEARCH,
900
                                              e -> find(),
901
                                              activeFileEditorIsNull );
902
    final Action editFindNextAction = new Action( Messages.get(
903
        "Main.menu.edit.find.next" ), "F3", null,
904
                                                  e -> findNext(),
905
                                                  activeFileEditorIsNull );
906
907
    // Insert actions
908
    final Action insertBoldAction = new Action( get( "Main.menu.insert.bold" ),
909
                                                "Shortcut+B", BOLD,
910
                                                e -> getActiveEditor().surroundSelection(
911
                                                    "**", "**" ),
912
                                                activeFileEditorIsNull );
913
    final Action insertItalicAction = new Action(
914
        get( "Main.menu.insert.italic" ), "Shortcut+I", ITALIC,
915
        e -> getActiveEditor().surroundSelection( "*", "*" ),
916
        activeFileEditorIsNull );
917
    final Action insertSuperscriptAction = new Action( get(
918
        "Main.menu.insert.superscript" ), "Shortcut+[", SUPERSCRIPT,
919
                                                       e -> getActiveEditor().surroundSelection(
920
                                                           "^", "^" ),
921
                                                       activeFileEditorIsNull );
922
    final Action insertSubscriptAction = new Action( get(
923
        "Main.menu.insert.subscript" ), "Shortcut+]", SUBSCRIPT,
924
                                                     e -> getActiveEditor().surroundSelection(
925
                                                         "~", "~" ),
926
                                                     activeFileEditorIsNull );
927
    final Action insertStrikethroughAction = new Action( get(
928
        "Main.menu.insert.strikethrough" ), "Shortcut+T", STRIKETHROUGH,
929
                                                         e -> getActiveEditor().surroundSelection(
930
                                                             "~~", "~~" ),
931
                                                         activeFileEditorIsNull );
932
    final Action insertBlockquoteAction = new Action( get(
933
        "Main.menu.insert.blockquote" ),
934
                                                      "Ctrl+Q",
935
                                                      QUOTE_LEFT,
936
                                                      // not Shortcut+Q
937
                                                      // because of conflict
938
                                                      // on Mac
939
                                                      e -> getActiveEditor().surroundSelection(
940
                                                          "\n\n> ", "" ),
941
                                                      activeFileEditorIsNull );
942
    final Action insertCodeAction = new Action( get( "Main.menu.insert.code" ),
943
                                                "Shortcut+K", CODE,
944
                                                e -> getActiveEditor().surroundSelection(
945
                                                    "`", "`" ),
946
                                                activeFileEditorIsNull );
947
    final Action insertFencedCodeBlockAction = new Action( get(
948
        "Main.menu.insert.fenced_code_block" ),
949
                                                           "Shortcut+Shift+K",
950
                                                           FILE_CODE_ALT,
951
                                                           e -> getActiveEditor()
952
                                                               .surroundSelection(
953
                                                                   "\n\n```\n",
954
                                                                   "\n```\n\n",
955
                                                                   get(
956
                                                                       "Main.menu.insert.fenced_code_block.prompt" ) ),
957
                                                           activeFileEditorIsNull );
958
959
    final Action insertLinkAction = new Action( get( "Main.menu.insert.link" ),
960
                                                "Shortcut+L", LINK,
961
                                                e -> getActiveEditor().insertLink(),
962
                                                activeFileEditorIsNull );
963
    final Action insertImageAction = new Action( get( "Main.menu.insert" +
964
                                                          ".image" ),
965
                                                 "Shortcut+G", PICTURE_ALT,
966
                                                 e -> getActiveEditor().insertImage(),
967
                                                 activeFileEditorIsNull );
968
969
    final Action[] headers = new Action[ 6 ];
970
971
    // Insert header actions (H1 ... H6)
972
    for( int i = 1; i <= 6; i++ ) {
973
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
974
      final String markup = String.format( "%n%n%s ", hashes );
975
      final String text = get( "Main.menu.insert.header_" + i );
976
      final String accelerator = "Shortcut+" + i;
977
      final String prompt = get( "Main.menu.insert.header_" + i + ".prompt" );
978
979
      headers[ i - 1 ] = new Action( text, accelerator, HEADER,
980
                                     e -> getActiveEditor().surroundSelection(
981
                                         markup, "", prompt ),
982
                                     activeFileEditorIsNull );
983
    }
984
985
    final Action insertUnorderedListAction = new Action(
986
        get( "Main.menu.insert.unordered_list" ), "Shortcut+U", LIST_UL,
987
        e -> getActiveEditor().surroundSelection( "\n\n* ", "" ),
988
        activeFileEditorIsNull );
989
    final Action insertOrderedListAction = new Action(
990
        get( "Main.menu.insert.ordered_list" ), "Shortcut+Shift+O", LIST_OL,
991
        e -> getActiveEditor().surroundSelection( "\n\n1. ", "" ),
992
        activeFileEditorIsNull );
993
    final Action insertHorizontalRuleAction = new Action(
994
        get( "Main.menu.insert.horizontal_rule" ), "Shortcut+H", null,
995
        e -> getActiveEditor().surroundSelection( "\n\n---\n\n", "" ),
996
        activeFileEditorIsNull );
997
998
    // R actions
999
    final Action mRScriptAction = new Action(
1000
        get( "Main.menu.r.script" ), null, null, e -> rScript() );
1001
1002
    final Action mRDirectoryAction = new Action(
1003
        get( "Main.menu.r.directory" ), null, null, e -> rDirectory() );
1004
1005
    // Help actions
1006
    final Action helpAboutAction = new Action(
1007
        get( "Main.menu.help.about" ), null, null, e -> helpAbout() );
1008
1009
    //---- MenuBar ----
1010
    final Menu fileMenu = ActionUtils.createMenu(
1011
        get( "Main.menu.file" ),
1012
        fileNewAction,
1013
        fileOpenAction,
1014
        null,
1015
        fileCloseAction,
1016
        fileCloseAllAction,
1017
        null,
1018
        fileSaveAction,
1019
        fileSaveAsAction,
1020
        fileSaveAllAction,
1021
        null,
1022
        fileExitAction );
1023
1024
    final Menu editMenu = ActionUtils.createMenu(
1025
        get( "Main.menu.edit" ),
1026
        editUndoAction,
1027
        editRedoAction,
1028
        editFindAction,
1029
        editFindNextAction );
1030
1031
    final Menu insertMenu = ActionUtils.createMenu(
1032
        get( "Main.menu.insert" ),
1033
        insertBoldAction,
1034
        insertItalicAction,
1035
        insertSuperscriptAction,
1036
        insertSubscriptAction,
1037
        insertStrikethroughAction,
1038
        insertBlockquoteAction,
1039
        insertCodeAction,
1040
        insertFencedCodeBlockAction,
1041
        null,
1042
        insertLinkAction,
1043
        insertImageAction,
1044
        null,
1045
        headers[ 0 ],
1046
        headers[ 1 ],
1047
        headers[ 2 ],
1048
        headers[ 3 ],
1049
        headers[ 4 ],
1050
        headers[ 5 ],
1051
        null,
1052
        insertUnorderedListAction,
1053
        insertOrderedListAction,
1054
        insertHorizontalRuleAction );
1055
1056
    final Menu rMenu = ActionUtils.createMenu(
1057
        get( "Main.menu.r" ),
1058
        mRScriptAction,
1059
        mRDirectoryAction );
1060
1061
    final Menu helpMenu = ActionUtils.createMenu(
1062
        get( "Main.menu.help" ),
1063
        helpAboutAction );
1064
1065
    menuBar = new MenuBar( fileMenu,
1066
                           editMenu,
1067
                           insertMenu,
1068
                           rMenu,
1069
                           helpMenu );
1070
1071
    //---- ToolBar ----
1072
    ToolBar toolBar = ActionUtils.createToolBar(
1073
        fileNewAction,
1074
        fileOpenAction,
1075
        fileSaveAction,
1076
        null,
1077
        editUndoAction,
1078
        editRedoAction,
1079
        null,
1080
        insertBoldAction,
1081
        insertItalicAction,
1082
        insertSuperscriptAction,
1083
        insertSubscriptAction,
1084
        insertBlockquoteAction,
1085
        insertCodeAction,
1086
        insertFencedCodeBlockAction,
1087
        null,
1088
        insertLinkAction,
1089
        insertImageAction,
1090
        null,
1091
        headers[ 0 ],
1092
        null,
1093
        insertUnorderedListAction,
1094
        insertOrderedListAction );
1095
1096
    return new VBox( menuBar, toolBar );
1097
  }
1098
1099
  /**
1100
   * Creates a boolean property that is bound to another boolean value of the
1101
   * active editor.
1102
   */
1103
  private BooleanProperty createActiveBooleanProperty(
1104
      final Function<FileEditorTab, ObservableBooleanValue> func ) {
1105
1106
    final BooleanProperty b = new SimpleBooleanProperty();
1107
    final FileEditorTab tab = getActiveFileEditor();
1108
1109
    if( tab != null ) {
1110
      b.bind( func.apply( tab ) );
1111
    }
1112
1113
    getFileEditorPane().activeFileEditorProperty().addListener(
1114
        ( observable, oldFileEditor, newFileEditor ) -> {
1115
          b.unbind();
1116
1117
          if( newFileEditor != null ) {
1118
            b.bind( func.apply( newFileEditor ) );
1119
          }
1120
          else {
1121
            b.set( false );
1122
          }
1123
        }
1124
    );
1125
1126
    return b;
1127
  }
1128
1129
  private void initLayout() {
1130
    final Scene appScene = getScene();
1131
1132
    appScene.getStylesheets().add( STYLESHEET_SCENE );
1133
1134
    // TODO: Apply an XML syntax highlighting for XML files.
1135
//    appScene.getStylesheets().add( STYLESHEET_XML );
1136
    appScene.windowProperty().addListener(
1137
        ( observable, oldWindow, newWindow ) -> {
1138
          newWindow.setOnCloseRequest( e -> {
1139
            if( !getFileEditorPane().closeAllEditors() ) {
1140
              e.consume();
1141
            }
1142
          } );
1143
1144
          // Workaround JavaFX bug: deselect menubar if window loses focus.
1145
          newWindow.focusedProperty().addListener(
1146
              ( obs, oldFocused, newFocused ) -> {
1147
                if( !newFocused ) {
1148
                  // Send an ESC key event to the menubar
1149
                  this.menuBar.fireEvent(
1150
                      new KeyEvent(
1151
                          KEY_PRESSED, CHAR_UNDEFINED, "", ESCAPE,
1152
                          false, false, false, false ) );
1153
                }
1154
              }
1155
          );
1156
        }
1157
    );
1158
  }
1159
1160
  private void putPreference( final String key, final String value ) {
1161
    try {
1162
      getPreferences().put( key, value );
1163
    } catch( final Exception ex ) {
1164
      getNotifier().notify( ex );
1165
    }
1166
  }
1167
}
11168
A src/main/java/com/scrivenvar/Messages.java
1
/*
2
 * Copyright (c) 2016 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  * Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  * Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar;
28
29
import static com.scrivenvar.Constants.APP_BUNDLE_NAME;
30
31
import java.text.MessageFormat;
32
import java.util.ResourceBundle;
33
import java.util.Stack;
34
35
/**
36
 * Recursively resolves message properties. Property values can refer to other
37
 * properties using a <code>${var}</code> syntax.
38
 *
39
 * @author Karl Tauber, Dave Jarvis
40
 */
41
public class Messages {
42
43
  private static final ResourceBundle RESOURCE_BUNDLE =
44
      ResourceBundle.getBundle(
45
          APP_BUNDLE_NAME );
46
47
  private Messages() {
48
  }
49
50
  /**
51
   * Return the value of a resource bundle value after having resolved any
52
   * references to other bundle variables.
53
   *
54
   * @param props The bundle containing resolvable properties.
55
   * @param s     The value for a key to resolve.
56
   * @return The value of the key with all references recursively dereferenced.
57
   */
58
  @SuppressWarnings("SameParameterValue")
59
  private static String resolve( final ResourceBundle props, final String s ) {
60
    final int len = s.length();
61
    final Stack<StringBuilder> stack = new Stack<>();
62
63
    StringBuilder sb = new StringBuilder( 256 );
64
    boolean open = false;
65
66
    for( int i = 0; i < len; i++ ) {
67
      final char c = s.charAt( i );
68
69
      switch( c ) {
70
        case '$': {
71
          if( i + 1 < len && s.charAt( i + 1 ) == '{' ) {
72
            stack.push( sb );
73
            sb = new StringBuilder( 256 );
74
            i++;
75
            open = true;
76
          }
77
78
          break;
79
        }
80
81
        case '}': {
82
          if( open ) {
83
            open = false;
84
            final String name = sb.toString();
85
86
            sb = stack.pop();
87
            sb.append( props.getString( name ) );
88
            break;
89
          }
90
        }
91
92
        default: {
93
          sb.append( c );
94
          break;
95
        }
96
      }
97
    }
98
99
    if( open ) {
100
      throw new IllegalArgumentException( "missing '}'" );
101
    }
102
103
    return sb.toString();
104
  }
105
106
  /**
107
   * Returns the value for a key from the message bundle.
108
   *
109
   * @param key Retrieve the value for this key.
110
   * @return The value for the key.
111
   */
112
  public static String get( String key ) {
113
    String result;
114
115
    try {
116
      result = resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
117
    } catch( final Exception ex ) {
118
      result = key;
119
    }
120
121
    return result;
122
  }
123
124
  public static String getLiteral( final String key ) {
125
    return RESOURCE_BUNDLE.getString( key );
126
  }
127
128
  /**
129
   * Returns the value for a key from the message bundle with the arguments
130
   * replacing <code>{#}</code> place holders.
131
   *
132
   * @param key  Retrieve the value for this key.
133
   * @param args The values to substitute for place holders.
134
   * @return The value for the key.
135
   */
136
  public static String get( String key, Object... args ) {
137
    return MessageFormat.format( get( key ), args );
138
  }
139
}
1140
A src/main/java/com/scrivenvar/Services.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
import java.util.ServiceLoader;
33
34
/**
35
 * Responsible for loading services. The services are treated as singleton
36
 * instances.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class Services {
41
42
  @SuppressWarnings("rawtypes")
43
  private static final Map<Class, Object> SINGLETONS = new HashMap<>();
44
45
  /**
46
   * Loads a service based on its interface definition. This will return an
47
   * existing instance if the class has already been instantiated.
48
   *
49
   * @param <T> The service to load.
50
   * @param api The interface definition for the service.
51
   * @return A class that implements the interface.
52
   */
53
  public static <T> T load( final Class<T> api ) {
54
    @SuppressWarnings("unchecked") final T o = (T) get( api );
55
56
    return o == null ? newInstance( api ) : o;
57
  }
58
59
  private static <T> T newInstance( final Class<T> api ) {
60
    final ServiceLoader<T> services = ServiceLoader.load( api );
61
62
    for( final T service : services ) {
63
      if( service != null ) {
64
        // Re-use the same instance the next time the class is loaded.
65
        put( api, service );
66
        return service;
67
      }
68
    }
69
70
    throw new RuntimeException( "No implementation for: " + api );
71
  }
72
73
  @SuppressWarnings("rawtypes")
74
  private static void put( final Class key, Object value ) {
75
    SINGLETONS.put( key, value );
76
  }
77
78
  @SuppressWarnings("rawtypes")
79
  private static Object get( final Class api ) {
80
    return SINGLETONS.get( api );
81
  }
82
}
183
A src/main/java/com/scrivenvar/controls/BrowseDirectoryButton.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.scrivenvar.controls;
29
30
import com.scrivenvar.Messages;
31
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
32
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
33
import javafx.event.ActionEvent;
34
import javafx.scene.control.Tooltip;
35
import javafx.stage.DirectoryChooser;
36
37
import java.io.File;
38
39
/**
40
 * Button that opens a directory chooser to select a local directory for a
41
 * URL in markdown.
42
 *
43
 * @author Karl Tauber
44
 */
45
public class BrowseDirectoryButton
46
    extends BrowseFileButton {
47
  public BrowseDirectoryButton() {
48
    setGraphic( FontAwesomeIconFactory.get()
49
                                      .createIcon( FontAwesomeIcon.FOLDER_ALT,
50
                                                   "1.2em" ) );
51
    setTooltip( new Tooltip( Messages.get( "BrowseDirectoryButton.tooltip" ) ) );
52
  }
53
54
  @Override
55
  protected void browse( ActionEvent e ) {
56
    DirectoryChooser directoryChooser = new DirectoryChooser();
57
    directoryChooser.setTitle( Messages.get(
58
        "BrowseDirectoryButton.chooser.title" ) );
59
    directoryChooser.setInitialDirectory( getInitialDirectory() );
60
    File result = directoryChooser.showDialog( getScene().getWindow() );
61
    if( result != null ) {
62
      updateUrl( result );
63
    }
64
  }
65
}
166
A src/main/java/com/scrivenvar/controls/BrowseFileButton.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.scrivenvar.controls;
29
30
import java.io.File;
31
import java.nio.file.Path;
32
import java.util.ArrayList;
33
import java.util.List;
34
import javafx.beans.property.ObjectProperty;
35
import javafx.beans.property.SimpleObjectProperty;
36
import javafx.event.ActionEvent;
37
import javafx.scene.control.Button;
38
import javafx.scene.control.Tooltip;
39
import javafx.scene.input.KeyCode;
40
import javafx.scene.input.KeyEvent;
41
import javafx.stage.FileChooser;
42
import javafx.stage.FileChooser.ExtensionFilter;
43
import com.scrivenvar.Messages;
44
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
45
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
46
47
/**
48
 * Button that opens a file chooser to select a local file for a URL in markdown.
49
 *
50
 * @author Karl Tauber
51
 */
52
public class BrowseFileButton
53
	extends Button
54
{
55
	private final List<ExtensionFilter> extensionFilters = new ArrayList<>();
56
57
	public BrowseFileButton() {
58
		setGraphic(FontAwesomeIconFactory.get().createIcon(FontAwesomeIcon.FILE_ALT, "1.2em"));
59
		setTooltip(new Tooltip(Messages.get("BrowseFileButton.tooltip")));
60
		setOnAction(this::browse);
61
62
		disableProperty().bind(basePath.isNull());
63
64
		// workaround for a JavaFX bug:
65
		//   avoid closing the dialog that contains this control when the user
66
		//   closes the FileChooser or DirectoryChooser using the ESC key
67
		addEventHandler(KeyEvent.KEY_RELEASED, e-> {
68
			if (e.getCode() == KeyCode.ESCAPE)
69
				e.consume();
70
		});
71
	}
72
73
	public void addExtensionFilter(ExtensionFilter extensionFilter) {
74
		extensionFilters.add(extensionFilter);
75
	}
76
77
	// 'basePath' property
78
	private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>();
79
	public Path getBasePath() { return basePath.get(); }
80
	public void setBasePath(Path basePath) { this.basePath.set(basePath); }
81
	public ObjectProperty<Path> basePathProperty() { return basePath; }
82
83
	// 'url' property
84
	private final ObjectProperty<String> url = new SimpleObjectProperty<>();
85
	public String getUrl() { return url.get(); }
86
	public void setUrl(String url) { this.url.set(url); }
87
	public ObjectProperty<String> urlProperty() { return url; }
88
89
	protected void browse(ActionEvent e) {
90
		FileChooser fileChooser = new FileChooser();
91
		fileChooser.setTitle(Messages.get("BrowseFileButton.chooser.title"));
92
		fileChooser.getExtensionFilters().addAll(extensionFilters);
93
		fileChooser.getExtensionFilters().add(new ExtensionFilter(Messages.get("BrowseFileButton.chooser.allFilesFilter"), "*.*"));
94
		fileChooser.setInitialDirectory(getInitialDirectory());
95
		File result = fileChooser.showOpenDialog(getScene().getWindow());
96
		if (result != null)
97
			updateUrl(result);
98
	}
99
100
	protected File getInitialDirectory() {
101
		//TODO build initial directory based on current value of 'url' property
102
		return getBasePath().toFile();
103
	}
104
105
	protected void updateUrl(File file) {
106
		String newUrl;
107
		try {
108
			newUrl = getBasePath().relativize(file.toPath()).toString();
109
		} catch (IllegalArgumentException ex) {
110
			newUrl = file.toString();
111
		}
112
		url.set(newUrl.replace('\\', '/'));
113
	}
114
}
1115
A src/main/java/com/scrivenvar/controls/EscapeTextField.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.scrivenvar.controls;
29
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.scene.control.TextField;
33
import javafx.util.StringConverter;
34
35
/**
36
 * TextField that can escape/unescape characters for markdown.
37
 *
38
 * @author Karl Tauber and White Magic Software, Ltd.
39
 */
40
public class EscapeTextField extends TextField {
41
42
  public EscapeTextField() {
43
    escapedText.bindBidirectional(
44
        textProperty(),
45
        new StringConverter<>() {
46
          @Override
47
          public String toString( String object ) {
48
            return escape( object );
49
          }
50
51
          @Override
52
          public String fromString( String string ) {
53
            return unescape( string );
54
          }
55
        }
56
    );
57
    escapeCharacters.addListener(
58
        e -> escapedText.set( escape( textProperty().get() ) )
59
    );
60
  }
61
62
  // 'escapedText' property
63
  private final StringProperty escapedText = new SimpleStringProperty();
64
65
  public StringProperty escapedTextProperty() {
66
    return escapedText;
67
  }
68
69
  // 'escapeCharacters' property
70
  private final StringProperty escapeCharacters = new SimpleStringProperty();
71
72
  public String getEscapeCharacters() {
73
    return escapeCharacters.get();
74
  }
75
76
  public void setEscapeCharacters( String escapeCharacters ) {
77
    this.escapeCharacters.set( escapeCharacters );
78
  }
79
80
  private String escape( final String s ) {
81
    final String escapeChars = getEscapeCharacters();
82
83
    return isEmpty( escapeChars ) ? s :
84
        s.replaceAll( "([" + escapeChars.replaceAll(
85
            "(.)",
86
            "\\\\$1" ) + "])", "\\\\$1" );
87
  }
88
89
  private String unescape( final String s ) {
90
    final String escapeChars = getEscapeCharacters();
91
92
    return isEmpty( escapeChars ) ? s :
93
        s.replaceAll( "\\\\([" + escapeChars
94
            .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" );
95
  }
96
97
  private static boolean isEmpty( final String s ) {
98
    return s == null || s.isEmpty();
99
  }
100
}
1101
A src/main/java/com/scrivenvar/controls/FlagCheckBox.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.controls;
28
29
import javafx.beans.property.IntegerProperty;
30
import javafx.beans.property.SimpleIntegerProperty;
31
import javafx.scene.control.CheckBox;
32
33
/**
34
 * CheckBox that toggles a bit in an integer.
35
 *
36
 * @author Karl Tauber
37
 */
38
public class FlagCheckBox extends CheckBox {
39
40
  public FlagCheckBox() {
41
    setOnAction( e -> {
42
      if( isSelected() ) {
43
        setFlags( getFlags() | getFlag() );
44
      } else {
45
        setFlags( getFlags() & ~getFlag() );
46
      }
47
    } );
48
49
    flags.addListener( (obs, oldFlags, newFlags) -> {
50
      setSelected( (newFlags.intValue() & getFlag()) != 0 );
51
    } );
52
  }
53
54
  // 'flag' property
55
  private final IntegerProperty flag = new SimpleIntegerProperty();
56
57
  public int getFlag() {
58
    return flag.get();
59
  }
60
61
  public void setFlag( int flag ) {
62
    this.flag.set( flag );
63
  }
64
65
  public IntegerProperty flagProperty() {
66
    return flag;
67
  }
68
69
  // 'flags' property
70
  private final IntegerProperty flags = new SimpleIntegerProperty();
71
72
  public int getFlags() {
73
    return flags.get();
74
  }
75
76
  public void setFlags( int flags ) {
77
    this.flags.set( flags );
78
  }
79
80
  public IntegerProperty flagsProperty() {
81
    return flags;
82
  }
83
}
184
A src/main/java/com/scrivenvar/controls/WebHyperlink.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.controls;
28
29
import com.scrivenvar.Main;
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.scene.control.Hyperlink;
33
34
/**
35
 * Opens a web site in the default web browser.
36
 *
37
 * @author Karl Tauber
38
 */
39
public class WebHyperlink extends Hyperlink {
40
41
  // 'uri' property
42
  private final StringProperty uri = new SimpleStringProperty();
43
44
  public WebHyperlink() {
45
    setStyle( "-fx-padding: 0; -fx-border-width: 0" );
46
  }
47
48
  @Override
49
  public void fire() {
50
    Main.showDocument( getUri() );
51
  }
52
53
  public String getUri() {
54
    return uri.get();
55
  }
56
57
  public void setUri( String uri ) {
58
    this.uri.set( uri );
59
  }
60
61
  public StringProperty uriProperty() {
62
    return uri;
63
  }
64
}
165
A src/main/java/com/scrivenvar/decorators/RVariableDecorator.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.decorators;
29
30
/**
31
 * Brackets variable names with <code>`r#</code> and <code>`</code>.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class RVariableDecorator implements VariableDecorator {
36
  public static final String PREFIX = "`r#";
37
  public static final char SUFFIX = '`';
38
39
  /**
40
   * Returns the given string R-escaping backticks prepended and appended. This
41
   * is not null safe. Do not pass null into this method.
42
   *
43
   * @param variableName The string to decorate.
44
   *
45
   * @return "`r#" + variableName + "`".
46
   */
47
  @Override
48
  public String decorate( final String variableName ) {
49
    return PREFIX +
50
        "x( v$" +
51
        variableName.replace( '.', '$' ) +
52
        " )" +
53
        SUFFIX;
54
  }
55
}
156
A src/main/java/com/scrivenvar/decorators/VariableDecorator.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.decorators;
29
30
/**
31
 * Responsible for updating variable names to use a machine-readable format
32
 * corresponding to the type of file being edited.
33
 */
34
public interface VariableDecorator {
35
36
  /**
37
   * This decorates a variable name based on some criteria determined by the
38
   * factory that creates implementations of this interface.
39
   *
40
   * @param variableName The text to decorate as per the filename extension
41
   *                     would indicate (e.g., ".md" goes to $VAR$ while "
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 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.decorators;
29
30
/**
31
 * Brackets variable names with dollar symbols.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class YamlVariableDecorator implements VariableDecorator {
36
37
  /**
38
   * Matches variables delimited by dollar symbols. The outer group is necessary
39
   * for substring replacement of delimited references.
40
   */
41
  public final static String REGEX = "(\\$(.*?)\\$)";
42
43
  /**
44
   * Returns the given string with a $ symbol prepended and appended. This is
45
   * not null safe. Do not pass null into this method.
46
   *
47
   * @param variableName The string to decorate.
48
   *
49
   * @return '$' + variableName + '$';
50
   */
51
  @Override
52
  public String decorate( final String variableName ) {
53
    return '$' + variableName + '$';
54
  }
55
}
156
A src/main/java/com/scrivenvar/definition/AbstractDefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.scene.control.TreeView;
31
32
/**
33
 * Implements common behaviour for definition sources.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public abstract class AbstractDefinitionSource implements DefinitionSource {
38
39
  private TreeView<String> mTreeView;
40
41
  /**
42
   * Returns this definition source as an editable graphical user interface
43
   * component.
44
   *
45
   * @return The TreeView for this definition source.
46
   */
47
  @Override
48
  public TreeView<String> asTreeView() {
49
50
    if( mTreeView == null ) {
51
      mTreeView = createTreeView();
52
      mTreeView.setEditable( true );
53
      mTreeView.setCellFactory(
54
          ( TreeView<String> t ) -> new TextFieldTreeCell()
55
      );
56
    }
57
58
    return mTreeView;
59
  }
60
61
  /**
62
   * Creates a newly instantiated tree view ready for adding to the definition
63
   * pane.
64
   *
65
   * @return A new tree view instance, never null.
66
   */
67
  protected abstract TreeView<String> createTreeView();
68
69
  /**
70
   * Ensures that when preferences are saved that an
71
   * {@link EmptyDefinitionSource} does not get saved literally as its
72
   * memory reference (the default value returned by {@link Object#toString()}).
73
   *
74
   * @return Empty string.
75
   */
76
  @Override
77
  public String toString() {
78
    return "";
79
  }
80
}
181
A src/main/java/com/scrivenvar/definition/DefinitionFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.FileType;
32
import com.scrivenvar.definition.yaml.YamlFileDefinitionSource;
33
34
import java.io.File;
35
import java.net.URI;
36
import java.net.URL;
37
import java.nio.file.Path;
38
import java.nio.file.Paths;
39
40
import static com.scrivenvar.Constants.*;
41
import static com.scrivenvar.FileType.YAML;
42
43
/**
44
 * Responsible for creating objects that can read and write definition data
45
 * sources. The data source could be YAML, TOML, JSON, flat files, or from a
46
 * database.
47
 *
48
 * @author White Magic Software, Ltd.
49
 */
50
public class DefinitionFactory extends AbstractFileFactory {
51
52
  /**
53
   * Default (empty) constructor.
54
   */
55
  public DefinitionFactory() {
56
  }
57
58
  /**
59
   * Creates a definition source capable of reading definitions from the given
60
   * path.
61
   *
62
   * @param path Path to a resource containing definitions.
63
   * @return The definition source appropriate for the given path.
64
   */
65
  public DefinitionSource createDefinitionSource( final String path ) {
66
    final String protocol = getProtocol( path );
67
    DefinitionSource result = null;
68
69
    if( DEFINITION_PROTOCOL_FILE.equals( protocol ) ) {
70
      final Path file = Paths.get( path );
71
      final FileType filetype = lookup( file, GLOB_PREFIX_DEFINITION );
72
      result = createFileDefinitionSource( filetype, file );
73
    }
74
    else {
75
      unknownFileType( protocol, path );
76
    }
77
78
    return result;
79
  }
80
81
  /**
82
   * Creates a definition source based on the file type.
83
   *
84
   * @param filetype Property key name suffix from settings.properties file.
85
   * @param path     Path to the file that corresponds to the extension.
86
   * @return A DefinitionSource capable of parsing the data stored at the path.
87
   */
88
  private DefinitionSource createFileDefinitionSource(
89
      final FileType filetype, final Path path ) {
90
91
    DefinitionSource result = null;
92
93
    if( filetype == YAML ) {
94
      result = new YamlFileDefinitionSource( path );
95
    }
96
    else {
97
      unknownFileType( filetype, path );
98
    }
99
100
    return result;
101
  }
102
103
  /**
104
   * Returns the protocol for a given URI or filename.
105
   *
106
   * @param source Determine the protocol for this URI or filename.
107
   * @return The protocol for the given source.
108
   */
109
  private String getProtocol( final String source ) {
110
    String protocol;
111
112
    try {
113
      final URI uri = new URI( source );
114
115
      if( uri.isAbsolute() ) {
116
        protocol = uri.getScheme();
117
      }
118
      else {
119
        final URL url = new URL( source );
120
        protocol = url.getProtocol();
121
      }
122
    } catch( final Exception e ) {
123
      // Could be HTTP, HTTPS?
124
      if( source.startsWith( "//" ) ) {
125
        throw new IllegalArgumentException( "Relative context: " + source );
126
      }
127
      else {
128
        final File file = new File( source );
129
        protocol = getProtocol( file );
130
      }
131
    }
132
133
    return protocol;
134
  }
135
136
  /**
137
   * Returns the protocol for a given file.
138
   *
139
   * @param file Determine the protocol for this file.
140
   * @return The protocol for the given file.
141
   */
142
  private String getProtocol( final File file ) {
143
    String result;
144
145
    try {
146
      result = file.toURI().toURL().getProtocol();
147
    } catch( final Exception e ) {
148
      result = DEFINITION_PROTOCOL_UNKNOWN;
149
    }
150
151
    return result;
152
  }
153
}
1154
A src/main/java/com/scrivenvar/definition/DefinitionPane.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import com.scrivenvar.AbstractPane;
31
32
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR_CHAR;
33
34
import com.scrivenvar.predicates.strings.ContainsPredicate;
35
import com.scrivenvar.predicates.strings.StartsPredicate;
36
import com.scrivenvar.predicates.strings.StringPredicate;
37
38
import static com.scrivenvar.util.Lists.getFirst;
39
40
import java.util.List;
41
42
import javafx.collections.ObservableList;
43
import javafx.event.EventHandler;
44
import javafx.event.EventType;
45
import javafx.scene.Node;
46
import javafx.scene.control.MultipleSelectionModel;
47
import javafx.scene.control.SelectionMode;
48
import javafx.scene.control.TreeItem;
49
import javafx.scene.control.TreeView;
50
import javafx.scene.input.MouseButton;
51
52
import static javafx.scene.input.MouseButton.PRIMARY;
53
54
import javafx.scene.input.MouseEvent;
55
56
import static javafx.scene.input.MouseEvent.MOUSE_CLICKED;
57
58
/**
59
 * Provides a list of variables that can be referenced in the editor.
60
 *
61
 * @author White Magic Software, Ltd.
62
 */
63
public class DefinitionPane extends AbstractPane {
64
65
  /**
66
   * Trimmed off the end of a word to match a variable name.
67
   */
68
  private final static String TERMINALS = ":;,.!?-/\\¡¿";
69
70
  private final TreeView<String> mTreeView;
71
72
  /**
73
   * Constructs a definition pane with a given tree view root.
74
   * See {@link com.scrivenvar.definition.yaml.YamlTreeAdapter#adapt(String)}
75
   * for details.
76
   *
77
   * @param root The root of the variable definition tree.
78
   */
79
  public DefinitionPane( final TreeView<String> root ) {
80
    assert root != null;
81
82
    mTreeView = root;
83
    initTreeView();
84
  }
85
86
  /**
87
   * Allows observers to receive double-click events on the tree view.
88
   *
89
   * @param handler The handler that will receive double-click events.
90
   */
91
  public void addBranchSelectedListener(
92
      final EventHandler<? super MouseEvent> handler ) {
93
    getTreeView().addEventHandler(
94
        MouseEvent.ANY, event -> {
95
          final MouseButton button = event.getButton();
96
          final int clicks = event.getClickCount();
97
          final EventType<? extends MouseEvent> eventType =
98
              event.getEventType();
99
100
          if( PRIMARY.equals( button ) && clicks == 2 ) {
101
            if( MOUSE_CLICKED.equals( eventType ) ) {
102
              handler.handle( event );
103
            }
104
105
            event.consume();
106
          }
107
        } );
108
  }
109
110
  /**
111
   * Allows observers to stop receiving double-click events on the tree view.
112
   *
113
   * @param handler The handler that will no longer receive double-click events.
114
   */
115
  public void removeBranchSelectedListener(
116
      final EventHandler<? super MouseEvent> handler ) {
117
    getTreeView().removeEventHandler( MouseEvent.ANY, handler );
118
  }
119
120
  /**
121
   * Changes the root node of the tree view. Swaps the current root node for the
122
   * root node of the given
123
   *
124
   * @param treeView The tree view containing a new root node; if the parameter
125
   *                 is null, the tree is cleared.
126
   */
127
  public void setRoot( final TreeView<String> treeView ) {
128
    getTreeView().setRoot( treeView == null ? null : treeView.getRoot() );
129
  }
130
131
  /**
132
   * Clears the tree view by setting the root node to null.
133
   */
134
  public void clear() {
135
    setRoot( null );
136
  }
137
138
  /**
139
   * Finds a tree item with a value that exactly matches the given word.
140
   *
141
   * @param trunk     The root item containing a list of nodes to search.
142
   * @param predicate Helps determine whether the node value matches the word.
143
   * @return The item that matches the given word, or null if not found.
144
   */
145
  private TreeItem<String> findNode(
146
      final TreeItem<String> trunk,
147
      final StringPredicate predicate ) {
148
    TreeItem<String> result = null;
149
150
    if( trunk != null ) {
151
      final List<TreeItem<String>> branches = trunk.getChildren();
152
153
      for( final TreeItem<String> leaf : branches ) {
154
        if( predicate.test( leaf.getValue() ) ) {
155
          result = leaf;
156
          break;
157
        }
158
      }
159
    }
160
161
    return result;
162
  }
163
164
  /**
165
   * Calls findNode with the EqualsPredicate. See
166
   * {@link #findNode(TreeItem, StringPredicate)} for details.
167
   *
168
   * @return The result from findNode.
169
   */
170
  private TreeItem<String> findStartsNode(
171
      final TreeItem<String> trunk,
172
      final String word ) {
173
    return findNode( trunk, new StartsPredicate( word ) );
174
  }
175
176
  /**
177
   * Calls findNode with the ContainsPredicate. See
178
   * {@link #findNode(TreeItem, StringPredicate)} for details.
179
   *
180
   * @return The result from findNode.
181
   */
182
  private TreeItem<String> findSubstringNode(
183
      final TreeItem<String> trunk,
184
      final String word ) {
185
    return findNode( trunk, new ContainsPredicate( word ) );
186
  }
187
188
  /**
189
   * Finds a node that matches a prefix and suffix specified by the given path
190
   * variable. The prefix must match a valid node value. The suffix refers to
191
   * the start of a string that matches zero or more children of the node
192
   * specified by the prefix. The algorithm has the following cases:
193
   *
194
   * <ol>
195
   * <li>Path is empty, return first child.</li>
196
   * <li>Path contains a complete match, return corresponding node.</li>
197
   * <li>Path contains a partial match, return nearest node.</li>
198
   * <li>Path contains a complete and partial match, return nearest node.</li>
199
   * </ol>
200
   *
201
   * @param word The word typed by the user, which contains dot-separated node
202
   *             names that represent a path within the YAML tree plus a
203
   *             partial variable
204
   *             name match (for a node).
205
   * @return The node value that starts with the suffix portion of the given
206
   * path, never null.
207
   */
208
  public TreeItem<String> findNode( final String word ) {
209
    String path = word;
210
211
    // Current tree item.
212
    TreeItem<String> cItem = getTreeRoot();
213
214
    // Previous tree item.
215
    TreeItem<String> pItem = cItem;
216
217
    int index = path.indexOf( SEPARATOR_CHAR );
218
219
    while( index >= 0 ) {
220
      final String node = path.substring( 0, index );
221
      path = path.substring( index + 1 );
222
223
      if( (cItem = findStartsNode( cItem, node )) == null ) {
224
        break;
225
      }
226
227
      index = path.indexOf( SEPARATOR_CHAR );
228
      pItem = cItem;
229
    }
230
231
    // Find the node that starts with whatever the user typed.
232
    cItem = findStartsNode( pItem, path );
233
234
    // If there was no matching node, then find a substring match.
235
    if( cItem == null ) {
236
      cItem = findSubstringNode( pItem, path );
237
    }
238
239
    // If neither starts with nor substring matched a node, revert to the last
240
    // known valid node.
241
    if( cItem == null ) {
242
      cItem = pItem;
243
    }
244
245
    return sanitize( cItem );
246
  }
247
248
  /**
249
   * Returns the leaf that matches the given value. If the value is terminally
250
   * punctuated, the punctuation is removed if no match was found.
251
   *
252
   * @param value The value to find, never null.
253
   * @return The leaf that contains the given value, or null if neither the
254
   * original value nor the terminally-trimmed value was found.
255
   */
256
  public VariableTreeItem<String> findLeaf( final String value ) {
257
    return findLeaf( value, false );
258
  }
259
260
  /**
261
   * Returns the leaf that matches the given value. If the value is terminally
262
   * punctuated, the punctuation is removed if no match was found.
263
   *
264
   * @param value    The value to find, never null.
265
   * @param contains Set to true to perform a substring match if starts with
266
   *                 fails to match.
267
   * @return The leaf that contains the given value, or null if neither the
268
   * original value nor the terminally-trimmed value was found.
269
   */
270
  public VariableTreeItem<String> findLeaf(
271
      final String value,
272
      final boolean contains ) {
273
274
    final VariableTreeItem<String> root = getTreeRoot();
275
    final VariableTreeItem<String> leaf = root.findLeaf( value, contains );
276
277
    return leaf == null
278
        ? root.findLeaf( rtrimTerminalPunctuation( value ) )
279
        : leaf;
280
  }
281
282
  /**
283
   * Removes punctuation from the end of a string. The character set includes:
284
   * <code>:;,.!?-/\¡¿</code>.
285
   *
286
   * @param s The string to trim, never null.
287
   * @return The string trimmed of all terminal characters from the end
288
   */
289
  private String rtrimTerminalPunctuation( final String s ) {
290
    final StringBuilder result = new StringBuilder( s.trim() );
291
292
    while( TERMINALS.contains( "" + result.charAt( result.length() - 1 ) ) ) {
293
      result.setLength( result.length() - 1 );
294
    }
295
296
    return result.toString();
297
  }
298
299
  /**
300
   * Returns the tree root if either item or its first child are null.
301
   *
302
   * @param item The item to make null safe.
303
   * @return A non-null TreeItem, possibly the root item (to avoid null).
304
   */
305
  private TreeItem<String> sanitize( final TreeItem<String> item ) {
306
    TreeItem<String> result;
307
308
    if( item == null ) {
309
      result = getTreeRoot();
310
    }
311
    else {
312
      result = item == getTreeRoot()
313
          ? getFirst( item.getChildren() )
314
          : item;
315
    }
316
317
    return result;
318
  }
319
320
  /**
321
   * Expands the node to the root, recursively.
322
   *
323
   * @param <T>  The type of tree item to expand (usually String).
324
   * @param node The node to expand.
325
   */
326
  public <T> void expand( final TreeItem<T> node ) {
327
    if( node != null ) {
328
      expand( node.getParent() );
329
330
      if( !node.isLeaf() ) {
331
        node.setExpanded( true );
332
      }
333
    }
334
  }
335
336
  public void select( final TreeItem<String> item ) {
337
    clearSelection();
338
    selectItem( getTreeView().getRow( item ) );
339
  }
340
341
  private void clearSelection() {
342
    getSelectionModel().clearSelection();
343
  }
344
345
  private void selectItem( final int row ) {
346
    getSelectionModel().select( row );
347
  }
348
349
  /**
350
   * Collapses the tree, recursively.
351
   */
352
  public void collapse() {
353
    collapse( getTreeRoot().getChildren() );
354
  }
355
356
  /**
357
   * Collapses the tree, recursively.
358
   *
359
   * @param <T>   The type of tree item to expand (usually String).
360
   * @param nodes The nodes to collapse.
361
   */
362
  private <T> void collapse( ObservableList<TreeItem<T>> nodes ) {
363
    for( final TreeItem<T> node : nodes ) {
364
      node.setExpanded( false );
365
      collapse( node.getChildren() );
366
    }
367
  }
368
369
  private void initTreeView() {
370
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
371
  }
372
373
  /**
374
   * Returns the root node to the tree view.
375
   *
376
   * @return getTreeView()
377
   */
378
  public Node getNode() {
379
    return getTreeView();
380
  }
381
382
  private MultipleSelectionModel getSelectionModel() {
383
    return getTreeView().getSelectionModel();
384
  }
385
386
  /**
387
   * Returns the tree view that contains the YAML definition hierarchy.
388
   *
389
   * @return A non-null instance.
390
   */
391
  private TreeView<String> getTreeView() {
392
    return mTreeView;
393
  }
394
395
  /**
396
   * Returns the root of the tree.
397
   *
398
   * @return The first node added to the YAML definition tree.
399
   */
400
  private VariableTreeItem<String> getTreeRoot() {
401
    final TreeItem<String> root = getTreeView().getRoot();
402
403
    return root instanceof VariableTreeItem ?
404
        (VariableTreeItem<String>) root : null;
405
  }
406
407
  public <T> boolean isRoot( final TreeItem<T> item ) {
408
    return getTreeRoot().equals( item );
409
  }
410
}
1411
A src/main/java/com/scrivenvar/definition/DefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.scene.control.TreeView;
31
32
import java.util.Map;
33
34
/**
35
 * Represents behaviours for reading and writing variable definitions.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public interface DefinitionSource {
40
41
  /**
42
   * Creates a TreeView from this definition source. The definition source is
43
   * responsible for observing the TreeView instance for changes and persisting
44
   * them, if needed.
45
   *
46
   * @return A hierarchical tree suitable for displaying in the definition pane.
47
   */
48
  TreeView<String> asTreeView();
49
50
  /**
51
   * Returns all the strings with their values resolved in a flat hierarchy.
52
   * This copies all the keys and resolved values into a new map.
53
   *
54
   * @return The new map created with all values having been resolved,
55
   * recursively.
56
   */
57
  Map<String, String> getResolvedMap();
58
59
  /**
60
   * Returns the error message, if any, that occurred while loading the
61
   * definition source.
62
   *
63
   * @return The empty string if no error occurred, otherwise the error message.
64
   */
65
  default String getError() {
66
    return "";
67
  }
68
69
  /**
70
   * Must return a re-loadable path to the data source. For a file, this is the
71
   * absolute file path. For a database, this could be the JDBC connection. For
72
   * a web site, this might be the GET URL.
73
   *
74
   * @return A non-null, non-empty string.
75
   */
76
  @Override
77
  String toString();
78
}
179
A src/main/java/com/scrivenvar/definition/EmptyDefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
import javafx.scene.control.TreeView;
33
34
/**
35
 * Creates a definition source that has no information to load or save.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public class EmptyDefinitionSource extends AbstractDefinitionSource {
40
41
  public EmptyDefinitionSource() {
42
  }
43
44
  @Override
45
  public Map<String, String> getResolvedMap() {
46
    return new HashMap<>();
47
  }
48
49
  @Override
50
  protected TreeView<String> createTreeView() {
51
    return new TreeView<>();
52
  }
53
}
154
A src/main/java/com/scrivenvar/definition/FileDefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import java.nio.file.Path;
31
32
/**
33
 * Implements common behaviour for file definition sources.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public abstract class FileDefinitionSource extends AbstractDefinitionSource {
38
39
  private Path mPath;
40
41
  /**
42
   * Constructs a new file definition source that can read and write data in the
43
   * hierarchical format contained within the file location specified by the
44
   * path.
45
   *
46
   * @param path Must not be null.
47
   */
48
  public FileDefinitionSource( final Path path ) {
49
    setPath( path );
50
  }
51
52
  private void setPath( final Path path ) {
53
    mPath = path;
54
  }
55
56
  public Path getPath() {
57
    return mPath;
58
  }
59
60
  /**
61
   * @return The path represented by this object.
62
   */
63
  @Override
64
  public String toString() {
65
    return getPath().toString();
66
  }
67
}
168
A src/main/java/com/scrivenvar/definition/TextFieldTreeCell.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.event.ActionEvent;
31
import javafx.scene.control.*;
32
import javafx.scene.input.KeyEvent;
33
34
import static com.scrivenvar.Messages.get;
35
36
/**
37
 * Provides behaviour of adding, removing, and editing tree view items.
38
 *
39
 * @author White Magic Software, Ltd.
40
 */
41
public class TextFieldTreeCell extends TreeCell<String> {
42
43
  private TextField textField;
44
  private final ContextMenu editMenu = new ContextMenu();
45
46
  public TextFieldTreeCell() {
47
    initEditMenu();
48
  }
49
50
  private void initEditMenu() {
51
    final MenuItem addItem = createMenuItem( "Definition.menu.add" );
52
    final MenuItem removeItem = createMenuItem( "Definition.menu.remove" );
53
54
    addItem.setOnAction( ( ActionEvent e ) -> {
55
      final VariableTreeItem<String> treeItem = new VariableTreeItem<>(
56
          "Undefined" );
57
      getTreeItem().getChildren().add( treeItem );
58
    } );
59
60
    removeItem.setOnAction( ( ActionEvent e ) -> {
61
      final TreeItem<?> c = getTreeItem();
62
      c.getParent().getChildren().remove( c );
63
    } );
64
65
    getEditMenu().getItems().add( addItem );
66
    getEditMenu().getItems().add( removeItem );
67
  }
68
69
  private ContextMenu getEditMenu() {
70
    return this.editMenu;
71
  }
72
73
  private MenuItem createMenuItem( String label ) {
74
    return new MenuItem( get( label ) );
75
  }
76
77
  @Override
78
  public void startEdit() {
79
    if( getTreeItem().isLeaf() ) {
80
      super.startEdit();
81
82
      final TextField inputField = getTextField();
83
84
      setText( null );
85
      setGraphic( inputField );
86
      inputField.selectAll();
87
      inputField.requestFocus();
88
    }
89
  }
90
91
  @Override
92
  public void cancelEdit() {
93
    super.cancelEdit();
94
95
    setText( getItem() );
96
    setGraphic( getTreeItem().getGraphic() );
97
  }
98
99
  @Override
100
  public void updateItem( String item, boolean empty ) {
101
    super.updateItem( item, empty );
102
103
    if( empty ) {
104
      setText( null );
105
      setGraphic( null );
106
    }
107
    else if( isEditing() ) {
108
      TextField tf = getTextField();
109
      tf.setText( getItemValue() );
110
111
      setText( null );
112
      setGraphic( tf );
113
    }
114
    else {
115
      setText( getItemValue() );
116
      setGraphic( getTreeItem().getGraphic() );
117
118
      if( !getTreeItem().isLeaf() && getTreeItem().getParent() != null ) {
119
        setContextMenu( getEditMenu() );
120
      }
121
    }
122
  }
123
124
  private TextField createTextField() {
125
    final TextField tf = new TextField( getItemValue() );
126
127
    tf.setOnKeyReleased( ( KeyEvent t ) -> {
128
      switch( t.getCode() ) {
129
        case ENTER:
130
          commitEdit( tf.getText() );
131
          break;
132
        case ESCAPE:
133
          cancelEdit();
134
          break;
135
      }
136
    } );
137
138
    return tf;
139
  }
140
141
  /**
142
   * Returns the item's text value.
143
   *
144
   * @return A non-null String, possibly empty.
145
   */
146
  private String getItemValue() {
147
    return getItem() == null ? "" : getItem();
148
  }
149
150
  private TextField getTextField() {
151
    if( this.textField == null ) {
152
      this.textField = createTextField();
153
    }
154
155
    return this.textField;
156
  }
157
}
1158
A src/main/java/com/scrivenvar/definition/VariableTreeItem.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import com.scrivenvar.decorators.VariableDecorator;
31
import com.scrivenvar.decorators.YamlVariableDecorator;
32
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR;
33
import static com.scrivenvar.editors.VariableNameInjector.DEFAULT_MAX_VAR_LENGTH;
34
import java.text.Normalizer;
35
import static java.text.Normalizer.Form.NFD;
36
import java.util.HashMap;
37
import java.util.Map;
38
import java.util.Stack;
39
import javafx.scene.control.TreeItem;
40
41
/**
42
 * Provides behaviour afforded to variable names and their corresponding value.
43
 *
44
 * @author White Magic Software, Ltd.
45
 * @param <T> The type of TreeItem (usually String).
46
 */
47
public class VariableTreeItem<T> extends TreeItem<T> {
48
49
  private final static int DEFAULT_MAP_SIZE = 1000;
50
51
  private final static VariableDecorator VARIABLE_DECORATOR
52
    = new YamlVariableDecorator();
53
54
  /**
55
   * Flattened tree.
56
   */
57
  private Map<String, String> map;
58
59
  /**
60
   * Constructs a new item with a default value.
61
   *
62
   * @param value Passed up to superclass.
63
   */
64
  public VariableTreeItem( final T value ) {
65
    super( value );
66
  }
67
68
  /**
69
   * Finds a leaf starting at the current node with text that matches the given
70
   * value.
71
   *
72
   * @param text The text to match against each leaf in the tree.
73
   *
74
   * @return The leaf that has a value starting with the given text.
75
   */
76
  public VariableTreeItem<T> findLeaf( final String text ) {
77
    return findLeaf( text, false );
78
  }
79
80
  /**
81
   * Finds a leaf starting at the current node with text that matches the given
82
   * value.
83
   *
84
   * @param text The text to match against each leaf in the tree.
85
   * @param contains Set to true to perform a substring match if starts with
86
   * fails.
87
   *
88
   * @return The leaf that has a value starting with the given text.
89
   */
90
  public VariableTreeItem<T> findLeaf(
91
    final String text,
92
    final boolean contains ) {
93
94
    final Stack<VariableTreeItem<T>> stack = new Stack<>();
95
    final VariableTreeItem<T> root = this;
96
97
    stack.push( root );
98
99
    boolean found = false;
100
    VariableTreeItem<T> node = null;
101
102
    while( !found && !stack.isEmpty() ) {
103
      node = stack.pop();
104
105
      if( contains && node.valueContains( text ) ) {
106
        found = true;
107
      }
108
      else if( !contains && node.valueStartsWith( text ) ) {
109
        found = true;
110
      }
111
      else {
112
        for( final TreeItem<T> child : node.getChildren() ) {
113
          stack.push( (VariableTreeItem<T>)child );
114
        }
115
116
        // No match found, yet.
117
        node = null;
118
      }
119
    }
120
121
    return node;
122
  }
123
124
  /**
125
   * Returns the value of the string without diacritic marks.
126
   * 
127
   * @return A non-null, possibly empty string.
128
   */
129
  private String getDiacriticlessValue() {
130
    final String value = getValue().toString();
131
    final String normalized = Normalizer.normalize(value, NFD);
132
    
133
    return normalized.replaceAll("\\p{M}", "");
134
  }
135
136
  /**
137
   * Returns true if this node is a leaf and its value starts with the given
138
   * text.
139
   *
140
   * @param s The text to compare against the node value.
141
   *
142
   * @return true Node is a leaf and its value starts with the given value.
143
   */
144
  private boolean valueStartsWith( final String s ) {
145
    return isLeaf() && getDiacriticlessValue().startsWith( s );
146
  }
147
148
  /**
149
   * Returns true if this node is a leaf and its value contains the given text.
150
   *
151
   * @param s The text to compare against the node value.
152
   *
153
   * @return true Node is a leaf and its value contains the given value.
154
   */
155
  private boolean valueContains( final String s ) {
156
    return isLeaf() && getDiacriticlessValue().contains( s );
157
  }
158
159
  /**
160
   * Returns the path for this node, with nodes made distinct using the
161
   * separator character. This uses two loops: one for pushing nodes onto a
162
   * stack and one for popping them off to create the path in desired order.
163
   *
164
   * @return A non-null string, possibly empty.
165
   */
166
  public String toPath() {
167
    final Stack<TreeItem<T>> stack = new Stack<>();
168
    TreeItem<T> node = this;
169
170
    while( node.getParent() != null ) {
171
      stack.push( node );
172
      node = node.getParent();
173
    }
174
175
    final StringBuilder sb = new StringBuilder( DEFAULT_MAX_VAR_LENGTH );
176
177
    while( !stack.isEmpty() ) {
178
      node = stack.pop();
179
180
      if( !node.isLeaf() ) {
181
        sb.append( node.getValue() );
182
183
        // This will add a superfluous separator, but instead of peeking at
184
        // the stack all the time, the last separator will be removed outside
185
        // the loop (one operation executed once).
186
        sb.append( SEPARATOR );
187
      }
188
    }
189
190
    // Remove the trailing SEPARATOR.
191
    if( sb.length() > 0 ) {
192
      sb.setLength( sb.length() - 1 );
193
    }
194
195
    return sb.toString();
196
  }
197
198
  /**
199
   * Returns the hierarchy, flattened to key-value pairs.
200
   *
201
   * @return A map of this tree's key-value pairs.
202
   */
203
  public Map<String, String> getMap() {
204
    if( this.map == null ) {
205
      this.map = new HashMap<>( DEFAULT_MAP_SIZE );
206
      populate( this, this.map );
207
    }
208
209
    return this.map;
210
  }
211
212
  private void populate( final TreeItem<T> parent, final Map<String, String> map ) {
213
    for( final TreeItem<T> child : parent.getChildren() ) {
214
      if( child.isLeaf() ) {
215
        @SuppressWarnings( "unchecked" )
216
        final String key = toVariable( ((VariableTreeItem<String>)child).toPath() );
217
        final String value = child.getValue().toString();
218
219
        map.put( key, value );
220
      }
221
      else {
222
        populate( child, map );
223
      }
224
    }
225
  }
226
227
  /**
228
   * Converts the name of the key to a simple variable by enclosing it with
229
   * dollar symbols.
230
   *
231
   * @param key The key name to change to a variable.
232
   *
233
   * @return $key$
234
   */
235
  public String toVariable( final String key ) {
236
    return VARIABLE_DECORATOR.decorate( key );
237
  }
238
}
1239
A src/main/java/com/scrivenvar/definition/yaml/YamlFileDefinitionSource.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import static com.scrivenvar.Messages.get;
31
import com.scrivenvar.definition.FileDefinitionSource;
32
import java.io.InputStream;
33
import java.nio.file.Files;
34
import java.nio.file.Path;
35
import java.util.Map;
36
import javafx.scene.control.TreeView;
37
38
/**
39
 * Represents a definition data source for YAML files.
40
 *
41
 * @author White Magic Software, Ltd.
42
 */
43
public class YamlFileDefinitionSource extends FileDefinitionSource {
44
45
  private YamlTreeAdapter yamlTreeAdapter;
46
  private YamlParser yamlParser;
47
48
  /**
49
   * Constructs a new YAML definition source, populated from the given file.
50
   *
51
   * @param path Path to the YAML definition file.
52
   */
53
  public YamlFileDefinitionSource( final Path path ) {
54
    super( path );
55
    init();
56
  }
57
  
58
  private void init() {
59
    setYamlParser( createYamlParser() );
60
  }
61
62
  @Override
63
  public Map<String, String> getResolvedMap() {
64
    return getYamlParser().createResolvedMap();
65
  }
66
67
  private YamlTreeAdapter getYamlTreeAdapter() {
68
    if( this.yamlTreeAdapter == null ) {
69
      setYamlTreeAdapter( new YamlTreeAdapter( getYamlParser() ) );
70
    }
71
72
    return this.yamlTreeAdapter;
73
  }
74
75
  private void setYamlTreeAdapter( final YamlTreeAdapter yamlTreeAdapter ) {
76
    this.yamlTreeAdapter = yamlTreeAdapter;
77
  }
78
79
  private YamlParser getYamlParser() {
80
    if( this.yamlParser == null ) {
81
      setYamlParser( createYamlParser() );
82
    }
83
84
    return this.yamlParser;
85
  }
86
87
  private void setYamlParser( final YamlParser yamlParser ) {
88
    this.yamlParser = yamlParser;
89
  }
90
91
  private YamlParser createYamlParser() {
92
    try( final InputStream in = Files.newInputStream( getPath() ) ) {
93
      return new YamlParser( in );
94
    } catch( final Exception ex ) {
95
      throw new RuntimeException( ex );
96
    }
97
  }
98
99
  @Override
100
  protected TreeView<String> createTreeView() {
101
    return getYamlTreeAdapter().adapt(
102
      get( "Pane.defintion.node.root.title" )
103
    );
104
  }
105
106
  @Override
107
  public String getError() {
108
    return getYamlParser().getError();
109
  }
110
}
1111
A src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import com.fasterxml.jackson.core.ObjectCodec;
31
import com.fasterxml.jackson.core.io.IOContext;
32
import com.fasterxml.jackson.databind.JsonNode;
33
import com.fasterxml.jackson.databind.ObjectMapper;
34
import com.fasterxml.jackson.databind.node.NullNode;
35
import com.fasterxml.jackson.databind.node.ObjectNode;
36
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
37
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
38
import com.scrivenvar.decorators.VariableDecorator;
39
import com.scrivenvar.decorators.YamlVariableDecorator;
40
import org.yaml.snakeyaml.DumperOptions;
41
42
import java.io.IOException;
43
import java.io.InputStream;
44
import java.io.Writer;
45
import java.text.MessageFormat;
46
import java.util.HashMap;
47
import java.util.Map;
48
import java.util.Map.Entry;
49
import java.util.regex.Matcher;
50
import java.util.regex.Pattern;
51
52
/**
53
 * <p>
54
 * This program loads a YAML document into memory, scans for variable
55
 * declarations, then substitutes any self-referential values back into the
56
 * document. Its output is the given YAML document without any variables.
57
 * Variables in the YAML document are denoted using a bracketed dollar symbol
58
 * syntax. For example: $field.name$. Some nomenclature to keep from going
59
 * squirrely, consider:
60
 * </p>
61
 *
62
 * <pre>
63
 *   root:
64
 *     node:
65
 *       name: $field.name$
66
 *   field:
67
 *     name: Alan Turing
68
 * </pre>
69
 * <p>
70
 * The various components of the given YAML are called:
71
 *
72
 * <ul>
73
 * <li><code>$field.name$</code> - delimited reference</li>
74
 * <li><code>field.name</code> - reference</li>
75
 * <li><code>name</code> - YAML field</li>
76
 * <li><code>Alan Turing</code> - (dereferenced) field value</li>
77
 * </ul>
78
 *
79
 * @author White Magic Software, Ltd.
80
 */
81
public class YamlParser {
82
83
  /**
84
   * Separates YAML variable nodes (e.g., the dots in
85
   * <code>$root.node.var$</code>).
86
   */
87
  public static final String SEPARATOR = ".";
88
  public static final char SEPARATOR_CHAR = SEPARATOR.charAt( 0 );
89
90
  private final static int GROUP_DELIMITED = 1;
91
  private final static int GROUP_REFERENCE = 2;
92
93
  private final static VariableDecorator VARIABLE_DECORATOR
94
      = new YamlVariableDecorator();
95
96
  private String mError;
97
98
  /**
99
   * Compiled version of DEFAULT_REGEX.
100
   */
101
  private final static Pattern REGEX_PATTERN
102
      = Pattern.compile( YamlVariableDecorator.REGEX );
103
104
  /**
105
   * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values.
106
   */
107
  private final static char SEPARATOR_YAML = '/';
108
109
  /**
110
   * Start of the Universe (the YAML document node that contains all others).
111
   */
112
  private JsonNode documentRoot;
113
114
  /**
115
   * Map of references to dereferenced field values.
116
   */
117
  private Map<String, String> references;
118
119
  public YamlParser( final InputStream in ) throws IOException {
120
    process( in );
121
  }
122
123
  /**
124
   * Returns the given string with all the delimited references swapped with
125
   * their recursively resolved values.
126
   *
127
   * @param text The text to parse with zero or more delimited references to
128
   *             replace.
129
   * @return The substituted value.
130
   */
131
  public String substitute( String text ) {
132
    final Matcher matcher = patternMatch( text );
133
    final Map<String, String> map = getReferences();
134
135
    while( matcher.find() ) {
136
      final String key = matcher.group( GROUP_DELIMITED );
137
      final String value = map.get( key );
138
139
      if( value == null ) {
140
        missing( text );
141
      }
142
      else {
143
        text = text.replace( key, value );
144
      }
145
    }
146
147
    return text;
148
  }
149
150
  /**
151
   * Returns all the strings with their values resolved in a flat hierarchy.
152
   * This copies all the keys and resolved values into a new map.
153
   *
154
   * @return The new map created with all values having been resolved,
155
   * recursively.
156
   */
157
  public Map<String, String> createResolvedMap() {
158
    final Map<String, String> map = new HashMap<>( 1024 );
159
160
    resolve( getDocumentRoot(), "", map );
161
162
    return map;
163
  }
164
165
  /**
166
   * Iterate over a given root node (at any level of the tree) and adapt each
167
   * leaf node.
168
   *
169
   * @param rootNode A JSON node (YAML node) to adapt.
170
   * @param map      Container that associates definitions with values.
171
   */
172
  private void resolve(
173
      final JsonNode rootNode,
174
      final String path,
175
      final Map<String, String> map ) {
176
177
    if( rootNode != null ) {
178
      rootNode.fields().forEachRemaining(
179
          ( Entry<String, JsonNode> leaf ) -> resolve( leaf, path, map )
180
      );
181
    }
182
  }
183
184
  /**
185
   * Recursively adapt each rootNode to a corresponding rootItem.
186
   *
187
   * @param rootNode The node to adapt.
188
   */
189
  private void resolve(
190
      final Entry<String, JsonNode> rootNode,
191
      final String path,
192
      final Map<String, String> map ) {
193
194
    final JsonNode leafNode = rootNode.getValue();
195
    final String key = rootNode.getKey();
196
197
198
    if( leafNode.isValueNode() ) {
199
      final String value;
200
201
      if( leafNode instanceof NullNode ) {
202
        value = "";
203
      }
204
      else {
205
        value = rootNode.getValue().asText();
206
      }
207
208
      map.put( VARIABLE_DECORATOR.decorate( path + key ), substitute( value ) );
209
    }
210
211
    if( leafNode.isObject() ) {
212
      resolve( leafNode, path + key + SEPARATOR, map );
213
    }
214
  }
215
216
  /**
217
   * Reads the first document from the given stream of YAML data and returns a
218
   * corresponding object that represents the YAML hierarchy. The calling class
219
   * is responsible for closing the stream. Calling classes should use
220
   * <code>JsonNode.fields()</code> to walk through the YAML tree of fields.
221
   *
222
   * @param in The input stream containing YAML content.
223
   * @throws IOException Could not read the stream.
224
   */
225
  private void process( final InputStream in ) throws IOException {
226
    final ObjectNode root = (ObjectNode) getObjectMapper().readTree( in );
227
    setDocumentRoot( root );
228
    process( root );
229
  }
230
231
  /**
232
   * Iterate over a given root node (at any level of the tree) and process each
233
   * leaf node.
234
   *
235
   * @param root A node to process.
236
   */
237
  private void process( final JsonNode root ) {
238
    root.fields().forEachRemaining( this::process );
239
  }
240
241
  /**
242
   * Process the given field, which is a named node. This is where the
243
   * application does the up-front work of mapping references to their fully
244
   * recursively dereferenced values.
245
   *
246
   * @param field The named node.
247
   */
248
  private void process( final Entry<String, JsonNode> field ) {
249
    final JsonNode node = field.getValue();
250
251
    if( node.isObject() ) {
252
      process( node );
253
    }
254
    else {
255
      final JsonNode fieldValue = field.getValue();
256
257
      // Only basic data types can be parsed into variable values. For
258
      // node structures, YAML has a built-in mechanism.
259
      if( fieldValue.isValueNode() ) {
260
        try {
261
          resolve( fieldValue.asText() );
262
        } catch( StackOverflowError e ) {
263
          setError( "Unresolvable: " + node.textValue() + " = " + fieldValue );
264
        }
265
      }
266
    }
267
  }
268
269
  /**
270
   * Inserts the delimited references and field values into the cache. This will
271
   * overwrite existing references.
272
   *
273
   * @param fieldValue YAML field containing zero or more delimited references.
274
   *                   If it contains a delimited reference, the parameter is
275
   *                   modified with the
276
   *                   dereferenced value before it is returned.
277
   * @return fieldValue without delimited references.
278
   */
279
  private String resolve( String fieldValue ) {
280
    final Matcher matcher = patternMatch( fieldValue );
281
282
    while( matcher.find() ) {
283
      final String delimited = matcher.group( GROUP_DELIMITED );
284
      final String reference = matcher.group( GROUP_REFERENCE );
285
      final String dereference = resolve( lookup( reference ) );
286
287
      fieldValue = fieldValue.replace( delimited, dereference );
288
289
      // This will perform some superfluous calls by overwriting existing
290
      // items in the delimited reference map.
291
      put( delimited, dereference );
292
    }
293
294
    return fieldValue;
295
  }
296
297
  /**
298
   * Inserts a key/value pair into the references map. The map retains
299
   * references and dereferenced values found in the YAML. If the reference
300
   * already exists, this will overwrite with a new value.
301
   *
302
   * @param delimited    The variable name.
303
   * @param dereferenced The resolved value.
304
   */
305
  private void put( String delimited, String dereferenced ) {
306
    if( dereferenced.isEmpty() ) {
307
      missing( delimited );
308
    }
309
    else {
310
      getReferences().put( delimited, dereferenced );
311
    }
312
  }
313
314
  /**
315
   * Writes the modified YAML document to standard output.
316
   */
317
  private void writeDocument() throws IOException {
318
    getObjectMapper().writeValue( System.out, getDocumentRoot() );
319
  }
320
321
  /**
322
   * Called when a delimited reference is dereferenced to an empty string. This
323
   * should produce a warning for the user.
324
   *
325
   * @param delimited Delimited reference with no derived value.
326
   */
327
  private void missing( final String delimited ) {
328
    setError( MessageFormat.format( "Missing value for '{0}'.", delimited ) );
329
  }
330
331
  /**
332
   * Returns a REGEX_PATTERN matcher for the given text.
333
   *
334
   * @param text The text that contains zero or more instances of a
335
   *             REGEX_PATTERN that can be found using the regular expression.
336
   */
337
  private Matcher patternMatch( String text ) {
338
    return getPattern().matcher( text );
339
  }
340
341
  /**
342
   * Finds the YAML value for a reference.
343
   *
344
   * @param reference References a value in the YAML document.
345
   * @return The dereferenced value.
346
   */
347
  private String lookup( final String reference ) {
348
    return getDocumentRoot().at( asPath( reference ) ).asText();
349
  }
350
351
  /**
352
   * Converts a reference (not delimited) to a path that can be used to find a
353
   * value that should exist inside the YAML document.
354
   *
355
   * @param reference The reference to convert to a YAML document path.
356
   * @return The reference with a leading slash and its separator characters
357
   * converted to slashes.
358
   */
359
  private String asPath( final String reference ) {
360
    return SEPARATOR_YAML + reference.replace( getDelimitedSeparator(),
361
                                               SEPARATOR_YAML );
362
  }
363
364
  /**
365
   * Sets the parent node for the entire YAML document tree.
366
   *
367
   * @param documentRoot The parent node.
368
   */
369
  private void setDocumentRoot( final ObjectNode documentRoot ) {
370
    this.documentRoot = documentRoot;
371
  }
372
373
  /**
374
   * Returns the parent node for the entire YAML document tree.
375
   *
376
   * @return The parent node.
377
   */
378
  protected JsonNode getDocumentRoot() {
379
    return this.documentRoot;
380
  }
381
382
  /**
383
   * Returns the compiled regular expression REGEX_PATTERN used to match
384
   * delimited references.
385
   *
386
   * @return A compiled regex for use with the Matcher.
387
   */
388
  private Pattern getPattern() {
389
    return REGEX_PATTERN;
390
  }
391
392
  /**
393
   * @return The list of references mapped to dereferenced values.
394
   */
395
  private Map<String, String> getReferences() {
396
    if( this.references == null ) {
397
      this.references = createReferences();
398
    }
399
400
    return this.references;
401
  }
402
403
  /**
404
   * Subclasses can override this method to insert their own map.
405
   *
406
   * @return An empty HashMap, never null.
407
   */
408
  protected Map<String, String> createReferences() {
409
    return new HashMap<>();
410
  }
411
412
  private final class ResolverYAMLFactory extends YAMLFactory {
413
414
    private static final long serialVersionUID = 1L;
415
416
    @Override
417
    protected YAMLGenerator _createGenerator(
418
        final Writer out, final IOContext ctxt ) throws IOException {
419
420
      return new ResolverYAMLGenerator(
421
          ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
422
          out, _version );
423
    }
424
  }
425
426
  private class ResolverYAMLGenerator extends YAMLGenerator {
427
428
    public ResolverYAMLGenerator(
429
        final IOContext ctxt,
430
        final int jsonFeatures,
431
        final int yamlFeatures,
432
        final ObjectCodec codec,
433
        final Writer out,
434
        final DumperOptions.Version version ) throws IOException {
435
436
      super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
437
    }
438
439
    @Override
440
    public void writeString( final String text ) throws IOException {
441
      super.writeString( substitute( text ) );
442
    }
443
  }
444
445
  private YAMLFactory getYAMLFactory() {
446
    return new ResolverYAMLFactory();
447
  }
448
449
  private ObjectMapper getObjectMapper() {
450
    return new ObjectMapper( getYAMLFactory() );
451
  }
452
453
  /**
454
   * Returns the character used to separate YAML paths within delimited
455
   * references. This will return only the first character of the command line
456
   * parameter, if the default is overridden.
457
   *
458
   * @return A period by default.
459
   */
460
  private char getDelimitedSeparator() {
461
    return SEPARATOR.charAt( 0 );
462
  }
463
464
  private void setError( final String error ) {
465
    mError = error;
466
  }
467
468
  /**
469
   * Returns the last error message, if any, that occurred during parsing.
470
   *
471
   * @return The error message or the empty string if no error occurred.
472
   */
473
  public String getError() {
474
    return mError == null ? "" : mError;
475
  }
476
}
1477
A src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.scrivenvar.definition.VariableTreeItem;
32
import javafx.scene.control.TreeItem;
33
import javafx.scene.control.TreeView;
34
35
import java.util.Map.Entry;
36
37
/**
38
 * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
39
 * interface.
40
 *
41
 * @author White Magic Software, Ltd.
42
 */
43
public class YamlTreeAdapter {
44
45
  private YamlParser yamlParser;
46
47
  public YamlTreeAdapter( final YamlParser parser ) {
48
    setYamlParser( parser );
49
  }
50
51
  /**
52
   * Converts a YAML document to a TreeView based on the document keys. Only the
53
   * first document in the stream is adapted.
54
   *
55
   * @param name Root TreeItem node name.
56
   * @return A TreeView populated with all the keys in the YAML document.
57
   */
58
  public TreeView<String> adapt( final String name ) {
59
    final JsonNode rootNode = getYamlParser().getDocumentRoot();
60
    final TreeItem<String> rootItem = createTreeItem( name );
61
62
    rootItem.setExpanded( true );
63
    adapt( rootNode, rootItem );
64
    return new TreeView<>( rootItem );
65
  }
66
67
  /**
68
   * Iterate over a given root node (at any level of the tree) and adapt each
69
   * leaf node.
70
   *
71
   * @param rootNode A JSON node (YAML node) to adapt.
72
   * @param rootItem The tree item to use as the root when processing the node.
73
   */
74
  private void adapt(
75
      final JsonNode rootNode, final TreeItem<String> rootItem ) {
76
77
    rootNode.fields().forEachRemaining(
78
        ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem )
79
    );
80
  }
81
82
  /**
83
   * Recursively adapt each rootNode to a corresponding rootItem.
84
   *
85
   * @param rootNode The node to adapt.
86
   * @param rootItem The item to adapt using the node's key.
87
   */
88
  private void adapt(
89
      final Entry<String, JsonNode> rootNode,
90
      final TreeItem<String> rootItem ) {
91
92
    final JsonNode leafNode = rootNode.getValue();
93
    final String key = rootNode.getKey();
94
    final TreeItem<String> leaf = createTreeItem( key );
95
96
    if( leafNode.isValueNode() ) {
97
      leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) );
98
    }
99
100
    rootItem.getChildren().add( leaf );
101
102
    if( leafNode.isObject() ) {
103
      adapt( leafNode, leaf );
104
    }
105
  }
106
107
  /**
108
   * Creates a new tree item that can be added to the tree view.
109
   *
110
   * @param value The node's value.
111
   * @return A new tree item node, never null.
112
   */
113
  private TreeItem<String> createTreeItem( final String value ) {
114
    return new VariableTreeItem<>( value );
115
  }
116
117
  private YamlParser getYamlParser() {
118
    return this.yamlParser;
119
  }
120
121
  private void setYamlParser( final YamlParser yamlParser ) {
122
    this.yamlParser = yamlParser;
123
  }
124
}
1125
A src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYAMLFactory.java
1
/*
2
 * The MIT License
3
 *
4
 * Copyright 2017 White Magic Software, Ltd..
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy
7
 * of this software and associated documentation files (the "Software"), to deal
8
 * in the Software without restriction, including without limitation the rights
9
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
 * copies of the Software, and to permit persons to whom the Software is
11
 * furnished to do so, subject to the following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be included in
14
 * all copies or substantial portions of the Software.
15
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
 * THE SOFTWARE.
23
 */
24
package com.scrivenvar.definition.yaml.resolvers;
25
26
import com.fasterxml.jackson.core.io.IOContext;
27
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
28
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
29
import com.scrivenvar.definition.yaml.YamlParser;
30
import java.io.IOException;
31
import java.io.Writer;
32
33
34
/**
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public final class ResolverYAMLFactory extends YAMLFactory {
39
40
  private static final long serialVersionUID = 1L;
41
42
  private YamlParser yamlParser;
43
44
  public ResolverYAMLFactory( final YamlParser yamlParser ) {
45
    setYamlParser( yamlParser );
46
  }
47
48
  @Override
49
  protected YAMLGenerator _createGenerator(
50
    final Writer out, final IOContext ctxt ) throws IOException {
51
52
    return new ResolverYAMLGenerator(
53
      getYamlParser(),
54
      ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
55
      out, _version );
56
  }
57
58
  /**
59
   * Returns the YAML parser used when constructing this instance.
60
   * 
61
   * @return A non-null instance.
62
   */
63
  private YamlParser getYamlParser() {
64
    return this.yamlParser;
65
  }
66
67
  /**
68
   * Sets the YAML parser used when constructing this instance.
69
   * 
70
   * @param yamlParser A non-null instance.
71
   */
72
  private void setYamlParser( final YamlParser yamlParser ) {
73
    this.yamlParser = yamlParser;
74
  }
75
}
176
A src/main/java/com/scrivenvar/definition/yaml/resolvers/ResolverYAMLGenerator.java
1
/*
2
 * The MIT License
3
 *
4
 * Copyright 2017 White Magic Software, Ltd..
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy
7
 * of this software and associated documentation files (the "Software"), to deal
8
 * in the Software without restriction, including without limitation the rights
9
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
 * copies of the Software, and to permit persons to whom the Software is
11
 * furnished to do so, subject to the following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be included in
14
 * all copies or substantial portions of the Software.
15
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
 * THE SOFTWARE.
23
 */
24
package com.scrivenvar.definition.yaml.resolvers;
25
26
import com.fasterxml.jackson.core.JsonGenerationException;
27
import com.fasterxml.jackson.core.ObjectCodec;
28
import com.fasterxml.jackson.core.io.IOContext;
29
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
30
import com.scrivenvar.definition.yaml.YamlParser;
31
import java.io.IOException;
32
import java.io.Writer;
33
import org.yaml.snakeyaml.DumperOptions;
34
35
/**
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
/**
40
 * Intercepts the string writing functionality to resolve the definition
41
 * value.
42
 */
43
public class ResolverYAMLGenerator extends YAMLGenerator {
44
45
  private YamlParser yamlParser;
46
47
  public ResolverYAMLGenerator(
48
    final YamlParser yamlParser,
49
    final IOContext ctxt,
50
    final int jsonFeatures,
51
    final int yamlFeatures,
52
    final ObjectCodec codec,
53
    final Writer out,
54
    final DumperOptions.Version version ) throws IOException {
55
    super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
56
    setYamlParser( yamlParser );
57
  }
58
59
  @Override
60
  public void writeString( final String text )
61
    throws IOException, JsonGenerationException {
62
    final YamlParser parser = getYamlParser();
63
    super.writeString( parser.substitute( text ) );
64
  }
65
66
  private YamlParser getYamlParser() {
67
    return yamlParser;
68
  }
69
70
  private void setYamlParser( final YamlParser yamlParser ) {
71
    this.yamlParser = yamlParser;
72
  }
73
}
174
A src/main/java/com/scrivenvar/dialogs/AbstractDialog.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import static com.scrivenvar.Messages.get;
31
import com.scrivenvar.service.events.impl.ButtonOrderPane;
32
import static javafx.scene.control.ButtonType.CANCEL;
33
import static javafx.scene.control.ButtonType.OK;
34
import javafx.scene.control.Dialog;
35
import javafx.stage.Window;
36
37
/**
38
 * Superclass that abstracts common behaviours for all dialogs.
39
 *
40
 * @author White Magic Software, Ltd.
41
 * @param <T> The type of dialog to create (usually String).
42
 */
43
public abstract class AbstractDialog<T> extends Dialog<T> {
44
45
  /**
46
   * Ensures that all dialogs can be closed.
47
   *
48
   * @param owner The parent window of this dialog.
49
   * @param title The messages title to display in the title bar.
50
   */
51
  @SuppressWarnings( "OverridableMethodCallInConstructor" )
52
  public AbstractDialog( final Window owner, final String title ) {
53
    setTitle( get( title ) );
54
    setResizable( true );
55
56
    initOwner( owner );
57
    initCloseAction();
58
    initDialogPane();
59
    initDialogButtons();
60
    initComponents();
61
  }
62
63
  /**
64
   * Initialize the component layout.
65
   */
66
  protected abstract void initComponents();
67
68
  /**
69
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
70
   */
71
  protected void initDialogPane() {
72
    setDialogPane( new ButtonOrderPane() );
73
  }
74
  
75
  /**
76
   * Set an OK and CANCEL button on the dialog.
77
   */
78
  protected void initDialogButtons() {
79
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
80
  }
81
82
  /**
83
   * Attaches a setOnCloseRequest to the dialog's [X] button so that the user
84
   * can always close the window, even if there's an error.
85
   */
86
  protected final void initCloseAction() {
87
    final Window window = getDialogPane().getScene().getWindow();
88
    window.setOnCloseRequest( event -> window.hide() );
89
  }
90
}
191
A src/main/java/com/scrivenvar/dialogs/ImageDialog.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.dialogs;
28
29
import static com.scrivenvar.Messages.get;
30
import com.scrivenvar.controls.BrowseFileButton;
31
import com.scrivenvar.controls.EscapeTextField;
32
import java.nio.file.Path;
33
import javafx.application.Platform;
34
import javafx.beans.binding.Bindings;
35
import javafx.beans.property.SimpleStringProperty;
36
import javafx.beans.property.StringProperty;
37
import javafx.scene.control.ButtonBar.ButtonData;
38
import static javafx.scene.control.ButtonType.OK;
39
import javafx.scene.control.DialogPane;
40
import javafx.scene.control.Label;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
import javafx.stage.Window;
43
import org.tbee.javafx.scene.layout.fxml.MigPane;
44
45
/**
46
 * Dialog to enter a markdown image.
47
 *
48
 * @author Karl Tauber
49
 */
50
public class ImageDialog extends AbstractDialog<String> {
51
52
  private final StringProperty image = new SimpleStringProperty();
53
54
  public ImageDialog( final Window owner, final Path basePath ) {
55
    super(owner, "Dialog.image.title" );
56
    
57
    final DialogPane dialogPane = getDialogPane();
58
    dialogPane.setContent( pane );
59
60
    linkBrowseFileButton.setBasePath( basePath );
61
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
62
    linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
63
64
    dialogPane.lookupButton( OK ).disableProperty().bind(
65
      urlField.escapedTextProperty().isEmpty()
66
      .or( textField.escapedTextProperty().isEmpty() ) );
67
68
    image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
69
      .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
70
      .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
71
    previewField.textProperty().bind( image );
72
73
    setResultConverter( dialogButton -> {
74
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
75
      return (data == ButtonData.OK_DONE) ? image.get() : null;
76
    } );
77
78
    Platform.runLater( () -> {
79
      urlField.requestFocus();
80
81
      if( urlField.getText().startsWith( "http://" ) ) {
82
        urlField.selectRange( "http://".length(), urlField.getLength() );
83
      }
84
    } );
85
  }
86
87
  @Override
88
  protected void initComponents() {
89
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
90
    pane = new MigPane();
91
    Label urlLabel = new Label();
92
    urlField = new EscapeTextField();
93
    linkBrowseFileButton = new BrowseFileButton();
94
    Label textLabel = new Label();
95
    textField = new EscapeTextField();
96
    Label titleLabel = new Label();
97
    titleField = new EscapeTextField();
98
    Label previewLabel = new Label();
99
    previewField = new Label();
100
101
    //======== pane ========
102
    {
103
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
104
      pane.setRows( "[][][][]" );
105
106
      //---- urlLabel ----
107
      urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
108
      pane.add( urlLabel, "cell 0 0" );
109
110
      //---- urlField ----
111
      urlField.setEscapeCharacters( "()" );
112
      urlField.setText( "http://yourlink.com" );
113
      urlField.setPromptText( "http://yourlink.com" );
114
      pane.add( urlField, "cell 1 0" );
115
      pane.add( linkBrowseFileButton, "cell 2 0" );
116
117
      //---- textLabel ----
118
      textLabel.setText( get( "Dialog.image.textLabel.text" ) );
119
      pane.add( textLabel, "cell 0 1" );
120
121
      //---- textField ----
122
      textField.setEscapeCharacters( "[]" );
123
      pane.add( textField, "cell 1 1 2 1" );
124
125
      //---- titleLabel ----
126
      titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
127
      pane.add( titleLabel, "cell 0 2" );
128
      pane.add( titleField, "cell 1 2 2 1" );
129
130
      //---- previewLabel ----
131
      previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
132
      pane.add( previewLabel, "cell 0 3" );
133
      pane.add( previewField, "cell 1 3 2 1" );
134
    }
135
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
136
  }
137
138
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
139
  private MigPane pane;
140
  private EscapeTextField urlField;
141
  private BrowseFileButton linkBrowseFileButton;
142
  private EscapeTextField textField;
143
  private EscapeTextField titleField;
144
  private Label previewField;
145
	// JFormDesigner - End of variables declaration  //GEN-END:variables
146
}
1147
A src/main/java/com/scrivenvar/dialogs/ImageDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "ImageDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "ImageDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
34
				name: "linkBrowseFileButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "javafx.scene.control.Label" ) {
39
				name: "textLabel"
40
				"text": new FormMessage( null, "ImageDialog.textLabel.text" )
41
				auxiliary() {
42
					"JavaCodeGenerator.variableLocal": true
43
				}
44
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
45
				"value": "cell 0 1"
46
			} )
47
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
48
				name: "textField"
49
				"escapeCharacters": "[]"
50
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
51
				"value": "cell 1 1 2 1"
52
			} )
53
			add( new FormComponent( "javafx.scene.control.Label" ) {
54
				name: "titleLabel"
55
				"text": new FormMessage( null, "ImageDialog.titleLabel.text" )
56
				auxiliary() {
57
					"JavaCodeGenerator.variableLocal": true
58
				}
59
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
60
				"value": "cell 0 2"
61
			} )
62
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
63
				name: "titleField"
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 1 2 2 1"
66
			} )
67
			add( new FormComponent( "javafx.scene.control.Label" ) {
68
				name: "previewLabel"
69
				"text": new FormMessage( null, "ImageDialog.previewLabel.text" )
70
				auxiliary() {
71
					"JavaCodeGenerator.variableLocal": true
72
				}
73
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
74
				"value": "cell 0 3"
75
			} )
76
			add( new FormComponent( "javafx.scene.control.Label" ) {
77
				name: "previewField"
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 1 3 2 1"
80
			} )
81
		}, new FormLayoutConstraints( null ) {
82
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
83
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
84
		} )
85
	}
86
}
187
A src/main/java/com/scrivenvar/dialogs/LinkDialog.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import 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
 * @author Karl Tauber
49
 */
50
public class LinkDialog extends AbstractDialog<String> {
51
52
  private final StringProperty link = new SimpleStringProperty();
53
54
  public LinkDialog(
55
    final Window owner, final HyperlinkModel hyperlink ) {
56
    super( owner, "Dialog.link.title" );
57
58
    final DialogPane dialogPane = getDialogPane();
59
    dialogPane.setContent( pane );
60
61
    dialogPane.lookupButton( OK ).disableProperty().bind(
62
      urlField.escapedTextProperty().isEmpty() );
63
64
    textField.setText( hyperlink.getText() );
65
    urlField.setText( hyperlink.getUrl() );
66
    titleField.setText( hyperlink.getTitle() );
67
68
    link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
69
      .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
70
      .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
71
        .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
72
        .otherwise( urlField.escapedTextProperty() ) ) );
73
74
    setResultConverter( dialogButton -> {
75
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
76
      return (data == ButtonData.OK_DONE) ? link.get() : null;
77
    } );
78
79
    Platform.runLater( () -> {
80
      urlField.requestFocus();
81
      urlField.selectRange( 0, urlField.getLength() );
82
    } );
83
  }
84
85
  @Override
86
  protected void initComponents() {
87
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
88
    pane = new MigPane();
89
    Label urlLabel = new Label();
90
    urlField = new EscapeTextField();
91
    Label textLabel = new Label();
92
    textField = new EscapeTextField();
93
    Label titleLabel = new Label();
94
    titleField = new EscapeTextField();
95
96
    //======== pane ========
97
    {
98
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
99
      pane.setRows( "[][][][]" );
100
101
      //---- urlLabel ----
102
      urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
103
      pane.add( urlLabel, "cell 0 0" );
104
105
      //---- urlField ----
106
      urlField.setEscapeCharacters( "()" );
107
      pane.add( urlField, "cell 1 0" );
108
109
      //---- textLabel ----
110
      textLabel.setText( get( "Dialog.link.textLabel.text" ) );
111
      pane.add( textLabel, "cell 0 1" );
112
113
      //---- textField ----
114
      textField.setEscapeCharacters( "[]" );
115
      pane.add( textField, "cell 1 1 3 1" );
116
117
      //---- titleLabel ----
118
      titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
119
      pane.add( titleLabel, "cell 0 2" );
120
      pane.add( titleField, "cell 1 2 3 1" );
121
    }
122
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
123
  }
124
125
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
126
  private MigPane pane;
127
  private EscapeTextField urlField;
128
  private EscapeTextField textField;
129
  private EscapeTextField titleField;
130
  // JFormDesigner - End of variables declaration  //GEN-END:variables
131
}
1132
A src/main/java/com/scrivenvar/dialogs/LinkDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "LinkDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "LinkDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) {
34
				name: "linkBrowseDirectoyButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
39
				name: "linkBrowseFileButton"
40
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
41
				"value": "cell 3 0"
42
			} )
43
			add( new FormComponent( "javafx.scene.control.Label" ) {
44
				name: "textLabel"
45
				"text": new FormMessage( null, "LinkDialog.textLabel.text" )
46
				auxiliary() {
47
					"JavaCodeGenerator.variableLocal": true
48
				}
49
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
50
				"value": "cell 0 1"
51
			} )
52
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
53
				name: "textField"
54
				"escapeCharacters": "[]"
55
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
56
				"value": "cell 1 1 3 1"
57
			} )
58
			add( new FormComponent( "javafx.scene.control.Label" ) {
59
				name: "titleLabel"
60
				"text": new FormMessage( null, "LinkDialog.titleLabel.text" )
61
				auxiliary() {
62
					"JavaCodeGenerator.variableLocal": true
63
				}
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 0 2"
66
			} )
67
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
68
				name: "titleField"
69
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
70
				"value": "cell 1 2 3 1"
71
			} )
72
			add( new FormComponent( "javafx.scene.control.Label" ) {
73
				name: "previewLabel"
74
				"text": new FormMessage( null, "LinkDialog.previewLabel.text" )
75
				auxiliary() {
76
					"JavaCodeGenerator.variableLocal": true
77
				}
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 0 3"
80
			} )
81
			add( new FormComponent( "javafx.scene.control.Label" ) {
82
				name: "previewField"
83
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
84
				"value": "cell 1 3 3 1"
85
			} )
86
		}, new FormLayoutConstraints( null ) {
87
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
88
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
89
		} )
90
	}
91
}
192
A src/main/java/com/scrivenvar/dialogs/RScriptDialog.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import javafx.application.Platform;
31
import javafx.geometry.Insets;
32
import javafx.scene.control.Label;
33
import javafx.scene.control.TextArea;
34
import javafx.scene.layout.GridPane;
35
import javafx.stage.Window;
36
37
import static com.scrivenvar.Messages.get;
38
import static javafx.scene.control.ButtonType.OK;
39
40
/**
41
 * Responsible for managing the R startup script that is run when an R source
42
 * file is loaded.
43
 *
44
 * @author White Magic Software, Ltd.
45
 */
46
public class RScriptDialog extends AbstractDialog<String> {
47
48
  private TextArea mScriptArea;
49
  private final String mOriginalText;
50
51
  public RScriptDialog(
52
      final Window parent, final String title, final String script ) {
53
    super( parent, title );
54
    mOriginalText = script;
55
    getScriptArea().setText( script );
56
  }
57
58
  @Override
59
  protected void initComponents() {
60
    final GridPane grid = new GridPane();
61
    grid.setHgap( 10 );
62
    grid.setVgap( 10 );
63
    grid.setPadding( new Insets( 10, 10, 10, 10 ) );
64
65
    final Label label = new Label( get( "Dialog.r.script.content" ) );
66
67
    final TextArea textArea = getScriptArea();
68
    textArea.setEditable( true );
69
    textArea.setWrapText( true );
70
71
    grid.add( label, 0, 0 );
72
    grid.add( textArea, 0, 1 );
73
74
    getDialogPane().setContent( grid );
75
76
    Platform.runLater( textArea::requestFocus );
77
78
    setResultConverter(
79
        dialogButton -> dialogButton == OK ?
80
            textArea.getText() :
81
            getOriginalText()
82
    );
83
  }
84
85
  private TextArea getScriptArea() {
86
    if( mScriptArea == null ) {
87
      mScriptArea = new TextArea();
88
    }
89
90
    return mScriptArea;
91
  }
92
93
  private String getOriginalText() {
94
    return mOriginalText;
95
  }
96
}
197
A src/main/java/com/scrivenvar/editors/EditorPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.AbstractPane;
31
32
import java.nio.file.Path;
33
import java.util.function.Consumer;
34
35
import javafx.application.Platform;
36
import javafx.beans.property.ObjectProperty;
37
import javafx.beans.property.SimpleObjectProperty;
38
import javafx.beans.value.ChangeListener;
39
import javafx.event.Event;
40
import javafx.scene.control.ScrollPane;
41
import javafx.scene.input.InputEvent;
42
import org.fxmisc.flowless.VirtualizedScrollPane;
43
import org.fxmisc.richtext.StyleClassedTextArea;
44
import org.fxmisc.undo.UndoManager;
45
import org.fxmisc.wellbehaved.event.EventPattern;
46
import org.fxmisc.wellbehaved.event.InputMap;
47
48
import static org.fxmisc.wellbehaved.event.InputMap.consume;
49
50
import org.fxmisc.wellbehaved.event.Nodes;
51
52
/**
53
 * Represents common editing features for various types of text editors.
54
 *
55
 * @author White Magic Software, Ltd.
56
 */
57
public class EditorPane extends AbstractPane {
58
59
  private StyleClassedTextArea editor;
60
  private VirtualizedScrollPane<StyleClassedTextArea> scrollPane;
61
  private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
62
63
  /**
64
   * Set when entering variable edit mode; retrieved upon exiting.
65
   */
66
  private InputMap<InputEvent> nodeMap;
67
68
  @Override
69
  public void requestFocus() {
70
    Platform.runLater( () -> getEditor().requestFocus() );
71
  }
72
73
  public void undo() {
74
    getUndoManager().undo();
75
  }
76
77
  public void redo() {
78
    getUndoManager().redo();
79
  }
80
81
  public UndoManager<?> getUndoManager() {
82
    return getEditor().getUndoManager();
83
  }
84
85
  public String getText() {
86
    return getEditor().getText();
87
  }
88
89
  public void setText( final String text ) {
90
    getEditor().deselect();
91
    getEditor().replaceText( text );
92
    getUndoManager().mark();
93
  }
94
95
  /**
96
   * Call to hook into changes to the text area.
97
   *
98
   * @param listener Receives editor text change events.
99
   */
100
  public void addTextChangeListener(
101
      final ChangeListener<? super String> listener ) {
102
    getEditor().textProperty().addListener( listener );
103
  }
104
105
  /**
106
   * Call to listen for when the caret moves to another paragraph.
107
   *
108
   * @param listener Receives paragraph change events.
109
   */
110
  public void addCaretParagraphListener(
111
      final ChangeListener<? super Integer> listener ) {
112
    getEditor().currentParagraphProperty().addListener( listener );
113
  }
114
115
  /**
116
   * This method adds listeners to editor events.
117
   *
118
   * @param <T>      The event type.
119
   * @param <U>      The consumer type for the given event type.
120
   * @param event    The event of interest.
121
   * @param consumer The method to call when the event happens.
122
   */
123
  public <T extends Event, U extends T> void addKeyboardListener(
124
      final EventPattern<? super T, ? extends U> event,
125
      final Consumer<? super U> consumer ) {
126
    Nodes.addInputMap( getEditor(), consume( event, consumer ) );
127
  }
128
129
  /**
130
   * This method adds listeners to editor events that can be removed without
131
   * affecting the original listeners (i.e., the original lister is restored on
132
   * a call to removeEventListener).
133
   *
134
   * @param map The map of methods to events.
135
   */
136
  @SuppressWarnings("unchecked")
137
  public void addEventListener( final InputMap<InputEvent> map ) {
138
    this.nodeMap = (InputMap<InputEvent>) getInputMap();
139
    Nodes.addInputMap( getEditor(), map );
140
  }
141
142
  /**
143
   * This method removes listeners to editor events and restores the default
144
   * handler.
145
   *
146
   * @param map The map of methods to events.
147
   */
148
  public void removeEventListener( final InputMap<InputEvent> map ) {
149
    Nodes.removeInputMap( getEditor(), map );
150
    Nodes.addInputMap( getEditor(), this.nodeMap );
151
  }
152
153
  /**
154
   * Returns the value for "org.fxmisc.wellbehaved.event.inputmap".
155
   *
156
   * @return An input map of input events.
157
   */
158
  private Object getInputMap() {
159
    return getEditor().getProperties().get( getInputMapKey() );
160
  }
161
162
  /**
163
   * Returns the hashmap key entry for the input map.
164
   *
165
   * @return "org.fxmisc.wellbehaved.event.inputmap"
166
   */
167
  private String getInputMapKey() {
168
    return "org.fxmisc.wellbehaved.event.inputmap";
169
  }
170
171
  /**
172
   * Repositions the cursor and scroll bar to the top of the file.
173
   */
174
  public void scrollToTop() {
175
    getEditor().moveTo( 0 );
176
    getScrollPane().scrollYToPixel( 0 );
177
  }
178
179
  private void setEditor( final StyleClassedTextArea textArea ) {
180
    this.editor = textArea;
181
  }
182
183
  public synchronized StyleClassedTextArea getEditor() {
184
    if( this.editor == null ) {
185
      setEditor( createTextArea() );
186
    }
187
188
    return this.editor;
189
  }
190
191
  /**
192
   * Returns the scroll pane that contains the text area.
193
   *
194
   * @return The scroll pane that contains the content to edit.
195
   */
196
  public synchronized VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
197
    if( this.scrollPane == null ) {
198
      this.scrollPane = createScrollPane();
199
    }
200
201
    return this.scrollPane;
202
  }
203
204
  protected VirtualizedScrollPane<StyleClassedTextArea> createScrollPane() {
205
    final VirtualizedScrollPane<StyleClassedTextArea> pane
206
        = new VirtualizedScrollPane<>( getEditor() );
207
    pane.setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
208
209
    return pane;
210
  }
211
212
  protected StyleClassedTextArea createTextArea() {
213
    return new StyleClassedTextArea( false );
214
  }
215
216
  public Path getPath() {
217
    return this.path.get();
218
  }
219
220
  public void setPath( final Path path ) {
221
    this.path.set( path );
222
  }
223
}
1224
A src/main/java/com/scrivenvar/editors/VariableNameDecoratorFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.decorators.RVariableDecorator;
32
import com.scrivenvar.decorators.VariableDecorator;
33
import com.scrivenvar.decorators.YamlVariableDecorator;
34
import java.nio.file.Path;
35
36
/**
37
 * Responsible for creating a variable name decorator suited to a particular
38
 * file type.
39
 *
40
 * @author White Magic Software, Ltd.
41
 */
42
public class VariableNameDecoratorFactory extends AbstractFileFactory {
43
44
  private VariableNameDecoratorFactory() {
45
  }
46
47
  public static VariableDecorator newInstance( final Path path ) {
48
    final VariableNameDecoratorFactory f = new VariableNameDecoratorFactory();
49
    final VariableDecorator result;
50
51
    switch( f.lookup( path ) ) {
52
      case RMARKDOWN:
53
      case RXML:
54
        result = new RVariableDecorator();
55
        break;
56
57
      default:
58
        result = new YamlVariableDecorator();
59
        break;
60
    }
61
62
    return result;
63
  }
64
}
165
A src/main/java/com/scrivenvar/editors/VariableNameInjector.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.FileEditorTab;
31
import com.scrivenvar.Services;
32
import com.scrivenvar.decorators.VariableDecorator;
33
import com.scrivenvar.definition.DefinitionPane;
34
import com.scrivenvar.definition.VariableTreeItem;
35
import com.scrivenvar.service.Settings;
36
import javafx.collections.ObservableList;
37
import javafx.event.Event;
38
import javafx.event.EventHandler;
39
import javafx.scene.control.IndexRange;
40
import javafx.scene.control.TreeItem;
41
import javafx.scene.control.TreeView;
42
import javafx.scene.input.InputEvent;
43
import javafx.scene.input.KeyCode;
44
import javafx.scene.input.KeyEvent;
45
import javafx.scene.input.MouseEvent;
46
import org.fxmisc.richtext.StyledTextArea;
47
import org.fxmisc.wellbehaved.event.EventPattern;
48
import org.fxmisc.wellbehaved.event.InputMap;
49
50
import java.nio.file.Path;
51
import java.util.function.Consumer;
52
53
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR;
54
import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR_CHAR;
55
import static com.scrivenvar.util.Lists.getFirst;
56
import static com.scrivenvar.util.Lists.getLast;
57
import static java.lang.Character.isWhitespace;
58
import static java.lang.Math.min;
59
import static javafx.scene.input.KeyCode.*;
60
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
61
import static javafx.scene.input.KeyCombination.SHIFT_DOWN;
62
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
63
import static org.fxmisc.wellbehaved.event.EventPattern.keyTyped;
64
import static org.fxmisc.wellbehaved.event.InputMap.consume;
65
import static org.fxmisc.wellbehaved.event.InputMap.sequence;
66
67
/**
68
 * Provides the logic for injecting variable names within the editor.
69
 *
70
 * @author White Magic Software, Ltd.
71
 */
72
public final class VariableNameInjector {
73
74
  public static final int DEFAULT_MAX_VAR_LENGTH = 64;
75
76
  private static final int NO_DIFFERENCE = -1;
77
78
  /**
79
   * TODO: Move this into settings.
80
   */
81
  private static final String PUNCTUATION = "\"#$%&'()*+,-/:;<=>?@[]^_`{|}~";
82
83
  private final Settings settings = Services.load( Settings.class );
84
85
  /**
86
   * Used to capture keyboard events once the user presses @.
87
   */
88
  private InputMap<InputEvent> keyboardMap;
89
90
  /**
91
   * Position of the variable in the text when in variable mode (0 by default).
92
   */
93
  private int initialCaretPosition;
94
95
  /**
96
   * Recipient of name injections.
97
   */
98
  private FileEditorTab tab;
99
100
  /**
101
   * Initiates double-click events.
102
   */
103
  private DefinitionPane definitionPane;
104
105
  private EventHandler<MouseEvent> panelEventHandler;
106
107
  /**
108
   * Initializes the variable name injector against the given pane.
109
   *
110
   * @param tab  The tab to inject variable names into.
111
   * @param pane The definition panel to listen to for double-click events.
112
   */
113
  public VariableNameInjector(
114
      final FileEditorTab tab, final DefinitionPane pane ) {
115
    setFileEditorTab( tab );
116
    setDefinitionPane( pane );
117
    initBranchSelectedListener();
118
    initKeyboardEventListeners();
119
  }
120
121
  /**
122
   * Traps double-click events on the definition pane.
123
   */
124
  private void initBranchSelectedListener() {
125
    final EventHandler<MouseEvent> eventHandler = getPanelEventHandler();
126
    getDefinitionPane().addBranchSelectedListener( eventHandler );
127
  }
128
129
  /**
130
   * Trap control+space and the @ key.
131
   *
132
   * @param tab The file editor that sends keyboard events for variable name
133
   *            injection.
134
   */
135
  public void initKeyboardEventListeners( final FileEditorTab tab ) {
136
    setFileEditorTab( tab );
137
    initKeyboardEventListeners();
138
  }
139
140
  /**
141
   * Traps keys for performing various short-cut tasks, such as @-mode variable
142
   * insertion and control+space for variable autocomplete.
143
   *
144
   * @ key is pressed, a new keyboard map is inserted in place of the current
145
   * map -- this class goes into "variable edit mode" (a.k.a. vMode).
146
   */
147
  private void initKeyboardEventListeners() {
148
    // Control and space are pressed.
149
    addKeyboardListener( keyPressed( SPACE, CONTROL_DOWN ),
150
                         this::autocomplete );
151
152
    // @ key in Linux?
153
    addKeyboardListener( keyPressed( DIGIT2, SHIFT_DOWN ), this::vMode );
154
    // @ key in Windows.
155
    addKeyboardListener( keyPressed( AT ), this::vMode );
156
  }
157
158
  /**
159
   * The @ symbol is a short-cut to inserting a YAML variable reference.
160
   *
161
   * @param e Superfluous information about the key that was pressed.
162
   */
163
  private void vMode( KeyEvent e ) {
164
    setInitialCaretPosition();
165
    vModeStart();
166
    vModeAutocomplete();
167
  }
168
169
  /**
170
   * Receives key presses until the user completes the variable selection. This
171
   * allows the arrow keys to be used for selecting variables.
172
   *
173
   * @param e The key that was pressed.
174
   */
175
  private void vModeKeyPressed( KeyEvent e ) {
176
    final KeyCode keyCode = e.getCode();
177
178
    switch( keyCode ) {
179
      case BACK_SPACE:
180
        // Don't decorate the variable upon exiting vMode.
181
        vModeBackspace();
182
        break;
183
184
      case ESCAPE:
185
        // Don't decorate the variable upon exiting vMode.
186
        vModeStop();
187
        break;
188
189
      case ENTER:
190
      case PERIOD:
191
      case RIGHT:
192
      case END:
193
        // Stop at a leaf node, ENTER means accept.
194
        if( vModeConditionalComplete() && keyCode == ENTER ) {
195
          vModeStop();
196
197
          // Decorate the variable upon exiting vMode.
198
          decorate();
199
        }
200
        break;
201
202
      case UP:
203
        cyclePathPrev();
204
        break;
205
206
      case DOWN:
207
        cyclePathNext();
208
        break;
209
210
      default:
211
        vModeFilterKeyPressed( e );
212
        break;
213
    }
214
215
    e.consume();
216
  }
217
218
  private void vModeBackspace() {
219
    deleteSelection();
220
221
    // Break out of variable mode by back spacing to the original position.
222
    if( getCurrentCaretPosition() > getInitialCaretPosition() ) {
223
      vModeAutocomplete();
224
    }
225
    else {
226
      vModeStop();
227
    }
228
  }
229
230
  /**
231
   * Updates the text with the path selected (or typed) by the user.
232
   */
233
  private void vModeAutocomplete() {
234
    final TreeItem<String> node = getCurrentNode();
235
236
    if( node != null && !node.isLeaf() ) {
237
      final String word = getLastPathWord();
238
      final String label = node.getValue();
239
      final int delta = difference( label, word );
240
      final String remainder = delta == NO_DIFFERENCE
241
          ? label
242
          : label.substring( delta );
243
244
      final StyledTextArea<?, ?> textArea = getEditor();
245
      final int posBegan = getCurrentCaretPosition();
246
      final int posEnded = posBegan + remainder.length();
247
248
      textArea.replaceSelection( remainder );
249
250
      if( posEnded - posBegan > 0 ) {
251
        textArea.selectRange( posEnded, posBegan );
252
      }
253
254
      expand( node );
255
    }
256
  }
257
258
  /**
259
   * Only variable name keys can pass through the filter. This is called when
260
   * the user presses a key.
261
   *
262
   * @param e The key that was pressed.
263
   */
264
  private void vModeFilterKeyPressed( final KeyEvent e ) {
265
    if( isVariableNameKey( e ) ) {
266
      typed( e.getText() );
267
    }
268
  }
269
270
  /**
271
   * Performs an autocomplete depending on whether the user has finished typing
272
   * in a word. If there is a selected range, then this will complete the most
273
   * recent word and jump to the next child.
274
   *
275
   * @return true The auto-completed node was a terminal node.
276
   */
277
  private boolean vModeConditionalComplete() {
278
    acceptPath();
279
280
    final TreeItem<String> node = getCurrentNode();
281
    final boolean terminal = isTerminal( node );
282
283
    if( !terminal ) {
284
      typed( SEPARATOR );
285
    }
286
287
    return terminal;
288
  }
289
290
  /**
291
   * Pressing control+space will find a node that matches the current word and
292
   * substitute the YAML variable reference. This is called when the user is not
293
   * editing in vMode.
294
   *
295
   * @param e Ignored -- it can only be Ctrl+Space.
296
   */
297
  private void autocomplete( final KeyEvent e ) {
298
    final String paragraph = getCaretParagraph();
299
    final int[] boundaries = getWordBoundaries( paragraph );
300
    final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
301
302
    VariableTreeItem<String> leaf = findLeaf( word );
303
304
    if( leaf == null ) {
305
      // If a leaf doesn't match using "starts with", then try using "contains".
306
      leaf = findLeaf( word, true );
307
    }
308
309
    if( leaf != null ) {
310
      replaceText( boundaries[ 0 ], boundaries[ 1 ], leaf.toPath() );
311
      decorate();
312
      expand( leaf );
313
    }
314
  }
315
316
  /**
317
   * Called when autocomplete finishes on a valid leaf or when the user presses
318
   * Enter to finish manual autocomplete.
319
   */
320
  private void decorate() {
321
    // A little bit of duplication...
322
    final String paragraph = getCaretParagraph();
323
    final int[] boundaries = getWordBoundaries( paragraph );
324
    final String old = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
325
326
    final String newVariable = decorate( old );
327
328
    final int posEnded = getCurrentCaretPosition();
329
    final int posBegan = posEnded - old.length();
330
331
    getEditor().replaceText( posBegan, posEnded, newVariable );
332
  }
333
334
  /**
335
   * Called when user double-clicks on a tree view item.
336
   *
337
   * @param variable The variable to decorate.
338
   */
339
  private String decorate( final String variable ) {
340
    return getVariableDecorator().decorate( variable );
341
  }
342
343
  /**
344
   * Inserts the given string at the current caret position, or replaces
345
   * selected text (if any).
346
   *
347
   * @param s The string to inject.
348
   */
349
  private void replaceSelection( final String s ) {
350
    getEditor().replaceSelection( s );
351
  }
352
353
  /**
354
   * Updates the text at the given position within the current paragraph.
355
   *
356
   * @param posBegan The starting index in the paragraph text to replace.
357
   * @param posEnded The ending index in the paragraph text to replace.
358
   * @param text     Overwrite the paragraph substring with this text.
359
   */
360
  private void replaceText(
361
      final int posBegan, final int posEnded, final String text ) {
362
    final int p = getCurrentParagraph();
363
364
    getEditor().replaceText( p, posBegan, p, posEnded, text );
365
  }
366
367
  /**
368
   * Returns the caret's current paragraph position.
369
   *
370
   * @return A number greater than or equal to 0.
371
   */
372
  private int getCurrentParagraph() {
373
    return getEditor().getCurrentParagraph();
374
  }
375
376
  /**
377
   * Returns current word boundary indexes into the current paragraph, excluding
378
   * punctuation.
379
   *
380
   * @param p      The paragraph wherein to hunt word boundaries.
381
   * @param offset The offset into the paragraph to begin scanning left and
382
   *               right.
383
   * @return The starting and ending index of the word closest to the caret.
384
   */
385
  private int[] getWordBoundaries( final String p, final int offset ) {
386
    // Remove dashes, but retain hyphens. Retain same number of characters
387
    // to preserve relative indexes.
388
    final String paragraph = p.replace( "---", "   " ).replace( "--", "  " );
389
390
    return getWordAt( paragraph, offset );
391
  }
392
393
  /**
394
   * Helper method to get the word boundaries for the current paragraph.
395
   *
396
   * @param paragraph The paragraph to search for word boundaries.
397
   * @return The word boundary indexes into the paragraph.
398
   */
399
  private int[] getWordBoundaries( final String paragraph ) {
400
    return getWordBoundaries( paragraph, getCurrentCaretColumn() );
401
  }
402
403
  /**
404
   * Given an arbitrary offset into a string, this returns the word at that
405
   * index. The inputs and outputs include:
406
   *
407
   * <ul>
408
   * <li>surrounded by space: <code>hello | world!</code> ("");</li>
409
   * <li>end of word: <code>hello| world!</code> ("hello");</li>
410
   * <li>start of a word: <code>hello |world!</code> ("world");</li>
411
   * <li>within a word: <code>hello wo|rld!</code> ("world");</li>
412
   * <li>end of a paragraph: <code>hello world!|</code> ("world");</li>
413
   * <li>start of a paragraph: <code>|hello world!</code> ("hello"); or</li>
414
   * <li>after punctuation: <code>hello world!|</code> ("world").</li>
415
   * </ul>
416
   *
417
   * @param p      The string to scan for a word.
418
   * @param offset The offset within s to begin searching for the nearest word
419
   *               boundary, must not be out of bounds of s.
420
   * @return The word in s at the offset.
421
   */
422
  private int[] getWordAt( final String p, final int offset ) {
423
    return new int[]{getWordBegan( p, offset ), getWordEnded( p, offset )};
424
  }
425
426
  /**
427
   * Returns the index into s where a word begins.
428
   *
429
   * @param s      Never null.
430
   * @param offset Index into s to begin searching backwards for a word
431
   *               boundary.
432
   * @return The index where a word begins.
433
   */
434
  private int getWordBegan( final String s, int offset ) {
435
    while( offset > 0 && isBoundary( s.charAt( offset - 1 ) ) ) {
436
      offset--;
437
    }
438
439
    return offset;
440
  }
441
442
  /**
443
   * Returns the index into s where a word ends.
444
   *
445
   * @param s      Never null.
446
   * @param offset Index into s to begin searching forwards for a word boundary.
447
   * @return The index where a word ends.
448
   */
449
  private int getWordEnded( final String s, int offset ) {
450
    final int length = s.length();
451
452
    while( offset < length && isBoundary( s.charAt( offset ) ) ) {
453
      offset++;
454
    }
455
456
    return offset;
457
  }
458
459
  /**
460
   * Returns true if the given character can be reasonably expected to be part
461
   * of a word, including punctuation marks.
462
   *
463
   * @param c The character to compare.
464
   * @return false The character is a space character.
465
   */
466
  private boolean isBoundary( final char c ) {
467
    return !isWhitespace( c ) && !isPunctuation( c );
468
  }
469
470
  /**
471
   * Returns true if the given character is part of the set of Latin (English)
472
   * punctuation marks.
473
   *
474
   * @param c The character to determine whether it is punctuation.
475
   * @return {@code true} when the given character is in the set of
476
   * {@link #PUNCTUATION}.
477
   */
478
  private static boolean isPunctuation( final char c ) {
479
    return PUNCTUATION.indexOf( c ) != -1;
480
  }
481
482
  /**
483
   * Returns the text for the paragraph that contains the caret.
484
   *
485
   * @return A non-null string, possibly empty.
486
   */
487
  private String getCaretParagraph() {
488
    return getEditor().getText( getCurrentParagraph() );
489
  }
490
491
  /**
492
   * Returns true if the node has children that can be selected (i.e., any
493
   * non-leaves).
494
   *
495
   * @param <T>  The type that the TreeItem contains.
496
   * @param node The node to test for terminality.
497
   * @return true The node has one branch and its a leaf.
498
   */
499
  private <T> boolean isTerminal( final TreeItem<T> node ) {
500
    final ObservableList<TreeItem<T>> branches = node.getChildren();
501
502
    return branches.size() == 1 && branches.get( 0 ).isLeaf();
503
  }
504
505
  /**
506
   * Inserts text that the user typed at the current caret position, then
507
   * performs an autocomplete for the variable name.
508
   *
509
   * @param text The text to insert, never null.
510
   */
511
  private void typed( final String text ) {
512
    getEditor().replaceSelection( text );
513
    vModeAutocomplete();
514
  }
515
516
  /**
517
   * Called when the user presses either End or Enter key.
518
   */
519
  private void acceptPath() {
520
    final IndexRange range = getSelectionRange();
521
522
    if( range != null ) {
523
      final int rangeEnd = range.getEnd();
524
      final StyledTextArea<?, ?> textArea = getEditor();
525
      textArea.deselect();
526
      textArea.moveTo( rangeEnd );
527
    }
528
  }
529
530
  /**
531
   * Replaces the entirety of the existing path (from the initial caret
532
   * position) with the given path.
533
   *
534
   * @param oldPath The path to replace.
535
   * @param newPath The replacement path.
536
   */
537
  private void replacePath( final String oldPath, final String newPath ) {
538
    final StyledTextArea<?, ?> textArea = getEditor();
539
    final int posBegan = getInitialCaretPosition();
540
    final int posEnded = posBegan + oldPath.length();
541
542
    textArea.deselect();
543
    textArea.replaceText( posBegan, posEnded, newPath );
544
  }
545
546
  /**
547
   * Called when the user presses the Backspace key.
548
   */
549
  private void deleteSelection() {
550
    final StyledTextArea<?, ?> textArea = getEditor();
551
    textArea.replaceSelection( "" );
552
    textArea.deletePreviousChar();
553
  }
554
555
  /**
556
   * Cycles the selected text through the nodes.
557
   *
558
   * @param direction true - next; false - previous
559
   */
560
  private void cycleSelection( final boolean direction ) {
561
    final TreeItem<String> node = getCurrentNode();
562
563
    // Find the sibling for the current selection and replace the current
564
    // selection with the sibling's value
565
    TreeItem<String> cycled = direction
566
        ? node.nextSibling()
567
        : node.previousSibling();
568
569
    // When cycling at the end (or beginning) of the list, jump to the first
570
    // (or last) sibling depending on the cycle direction.
571
    if( cycled == null ) {
572
      cycled = direction ? getFirstSibling( node ) : getLastSibling( node );
573
    }
574
575
    final String path = getCurrentPath();
576
    final String cycledWord = cycled.getValue();
577
    final String word = getLastPathWord();
578
    final int index = path.indexOf( word );
579
    final String cycledPath = path.substring( 0, index ) + cycledWord;
580
581
    expand( cycled );
582
    replacePath( path, cycledPath );
583
  }
584
585
  /**
586
   * Cycles to the next sibling of the currently selected tree node.
587
   */
588
  private void cyclePathNext() {
589
    cycleSelection( true );
590
  }
591
592
  /**
593
   * Cycles to the previous sibling of the currently selected tree node.
594
   */
595
  private void cyclePathPrev() {
596
    cycleSelection( false );
597
  }
598
599
  /**
600
   * Returns the variable name (or as much as has been typed so far). Returns
601
   * all the characters from the initial caret column to the the first
602
   * whitespace character. This will return a path that contains zero or more
603
   * separators.
604
   *
605
   * @return A non-null string, possibly empty.
606
   */
607
  private String getCurrentPath() {
608
    final String s = extractTextChunk();
609
    final int length = s.length();
610
611
    int i = 0;
612
613
    while( i < length && !isWhitespace( s.charAt( i ) ) ) {
614
      i++;
615
    }
616
617
    return s.substring( 0, i );
618
  }
619
620
  private <T> ObservableList<TreeItem<T>> getSiblings(
621
      final TreeItem<T> item ) {
622
    final TreeItem<T> parent = item.getParent();
623
    return parent == null ? item.getChildren() : parent.getChildren();
624
  }
625
626
  private <T> TreeItem<T> getFirstSibling( final TreeItem<T> item ) {
627
    return getFirst( getSiblings( item ), item );
628
  }
629
630
  private <T> TreeItem<T> getLastSibling( final TreeItem<T> item ) {
631
    return getLast( getSiblings( item ), item );
632
  }
633
634
  /**
635
   * Returns the caret position as an offset into the text.
636
   *
637
   * @return A value from 0 to the length of the text (minus one).
638
   */
639
  private int getCurrentCaretPosition() {
640
    return getEditor().getCaretPosition();
641
  }
642
643
  /**
644
   * Returns the caret position within the current paragraph.
645
   *
646
   * @return A value from 0 to the length of the current paragraph.
647
   */
648
  private int getCurrentCaretColumn() {
649
    return getEditor().getCaretColumn();
650
  }
651
652
  /**
653
   * Returns the last word from the path.
654
   *
655
   * @return The last token.
656
   */
657
  private String getLastPathWord() {
658
    String path = getCurrentPath();
659
660
    int i = path.indexOf( SEPARATOR_CHAR );
661
662
    while( i > 0 ) {
663
      path = path.substring( i + 1 );
664
      i = path.indexOf( SEPARATOR_CHAR );
665
    }
666
667
    return path;
668
  }
669
670
  /**
671
   * Returns text from the initial caret position until some arbitrarily long
672
   * number of characters. The number of characters extracted will be
673
   * getMaxVarLength, or fewer, depending on how many characters remain to be
674
   * extracted. The result from this method is trimmed to the first whitespace
675
   * character.
676
   *
677
   * @return A chunk of text that includes all the words representing a path,
678
   * and then some.
679
   */
680
  private String extractTextChunk() {
681
    final StyledTextArea<?, ?> textArea = getEditor();
682
    final int textBegan = getInitialCaretPosition();
683
    final int remaining = textArea.getLength() - textBegan;
684
    final int textEnded = min( remaining, getMaxVarLength() );
685
686
    try {
687
      return textArea.getText( textBegan, textEnded );
688
    } catch( final Exception e ) {
689
      return textArea.getText();
690
    }
691
  }
692
693
  /**
694
   * Returns the node for the current path.
695
   */
696
  private TreeItem<String> getCurrentNode() {
697
    return findNode( getCurrentPath() );
698
  }
699
700
  /**
701
   * Finds the node that most closely matches the given path.
702
   *
703
   * @param path The path that represents a node.
704
   * @return The node for the path, or the root node if the path could not be
705
   * found, but never null.
706
   */
707
  private TreeItem<String> findNode( final String path ) {
708
    return getDefinitionPane().findNode( path );
709
  }
710
711
  /**
712
   * Finds the first leaf having a value that starts with the given text.
713
   *
714
   * @param text The text to find in the definition tree.
715
   * @return The leaf that starts with the given text, or null if not found.
716
   */
717
  private VariableTreeItem<String> findLeaf( final String text ) {
718
    return getDefinitionPane().findLeaf( text, false );
719
  }
720
721
  /**
722
   * Finds the first leaf having a value that starts with the given text, or
723
   * contains the text if contains is true.
724
   *
725
   * @param text     The text to find in the definition tree.
726
   * @param contains Set true to perform a substring match after a starts with
727
   *                 match.
728
   * @return The leaf that starts with the given text, or null if not found.
729
   */
730
  @SuppressWarnings("SameParameterValue")
731
  private VariableTreeItem<String> findLeaf(
732
      final String text, final boolean contains ) {
733
    return getDefinitionPane().findLeaf( text, contains );
734
  }
735
736
  /**
737
   * Used to ignore typed keys in favour of trapping pressed keys.
738
   *
739
   * @param e The key that was typed.
740
   */
741
  private void vModeKeyTyped( KeyEvent e ) {
742
    e.consume();
743
  }
744
745
  /**
746
   * Used to lazily initialize the keyboard map.
747
   *
748
   * @return Mappings for keyTyped and keyPressed.
749
   */
750
  protected InputMap<InputEvent> createKeyboardMap() {
751
    return sequence(
752
        consume( keyTyped(), this::vModeKeyTyped ),
753
        consume( keyPressed(), this::vModeKeyPressed )
754
    );
755
  }
756
757
  private InputMap<InputEvent> getKeyboardMap() {
758
    if( this.keyboardMap == null ) {
759
      this.keyboardMap = createKeyboardMap();
760
    }
761
762
    return this.keyboardMap;
763
  }
764
765
  /**
766
   * Collapses the tree then expands and selects the given node.
767
   *
768
   * @param node The node to expand.
769
   */
770
  private void expand( final TreeItem<String> node ) {
771
    final DefinitionPane pane = getDefinitionPane();
772
    pane.collapse();
773
    pane.expand( node );
774
    pane.select( node );
775
  }
776
777
  /**
778
   * Returns true iff the key code the user typed can be used as part of a YAML
779
   * variable name.
780
   *
781
   * @param keyEvent Keyboard key press event information.
782
   * @return true The key is a value that can be inserted into the text.
783
   */
784
  private boolean isVariableNameKey( final KeyEvent keyEvent ) {
785
    final KeyCode kc = keyEvent.getCode();
786
787
    return (kc.isLetterKey()
788
        || kc.isDigitKey()
789
        || (keyEvent.isShiftDown() && kc == MINUS))
790
        && !keyEvent.isControlDown();
791
  }
792
793
  /**
794
   * Starts to capture user input events.
795
   */
796
  private void vModeStart() {
797
    addEventListener( getKeyboardMap() );
798
  }
799
800
  /**
801
   * Restores capturing of user input events to the previous event listener.
802
   * Also asks the processing chain to modify the variable text into a
803
   * machine-readable variable based on the format required by the file type.
804
   * For example, a Markdown file (.md) will substitute a $VAR$ name while an R
805
   * file (.Rmd, .Rxml) will use `r#xVAR`.
806
   */
807
  private void vModeStop() {
808
    removeEventListener( getKeyboardMap() );
809
  }
810
811
  /**
812
   * @return A variable decorator that corresponds to the given file type.
813
   */
814
  private VariableDecorator getVariableDecorator() {
815
    return VariableNameDecoratorFactory.newInstance( getFilename() );
816
  }
817
818
  private Path getFilename() {
819
    return getFileEditorTab().getPath();
820
  }
821
822
  /**
823
   * Returns the index where the two strings diverge.
824
   *
825
   * @param s1 The string that could be a substring of s2, null allowed.
826
   * @param s2 The string that could be a substring of s1, null allowed.
827
   * @return NO_DIFFERENCE if the strings are the same, otherwise the index
828
   * where they differ.
829
   */
830
  private int difference( final CharSequence s1, final CharSequence s2 ) {
831
    if( s1 == s2 ) {
832
      return NO_DIFFERENCE;
833
    }
834
835
    if( s1 == null || s2 == null ) {
836
      return 0;
837
    }
838
839
    int i = 0;
840
    final int limit = min( s1.length(), s2.length() );
841
842
    while( i < limit && s1.charAt( i ) == s2.charAt( i ) ) {
843
      i++;
844
    }
845
846
    // If one string was shorter than the other, that's where they differ.
847
    return i;
848
  }
849
850
  private EditorPane getEditorPane() {
851
    return getFileEditorTab().getEditorPane();
852
  }
853
854
  /**
855
   * Delegates to the file editor pane, and, ultimately, to its text area.
856
   */
857
  private <T extends Event, U extends T> void addKeyboardListener(
858
      final EventPattern<? super T, ? extends U> event,
859
      final Consumer<? super U> consumer ) {
860
    getEditorPane().addKeyboardListener( event, consumer );
861
  }
862
863
  /**
864
   * Delegates to the file editor pane, and, ultimately, to its text area.
865
   *
866
   * @param map The map of methods to events.
867
   */
868
  private void addEventListener( final InputMap<InputEvent> map ) {
869
    getEditorPane().addEventListener( map );
870
  }
871
872
  private void removeEventListener( final InputMap<InputEvent> map ) {
873
    getEditorPane().removeEventListener( map );
874
  }
875
876
  /**
877
   * Returns the position of the caret when variable mode editing was requested.
878
   *
879
   * @return The variable mode caret position.
880
   */
881
  private int getInitialCaretPosition() {
882
    return this.initialCaretPosition;
883
  }
884
885
  /**
886
   * Sets the position of the caret when variable mode editing was requested.
887
   * Stores the current position because only the text that comes afterwards is
888
   * a suitable variable reference.
889
   */
890
  private void setInitialCaretPosition() {
891
    this.initialCaretPosition = getEditor().getCaretPosition();
892
  }
893
894
  private StyledTextArea<?, ?> getEditor() {
895
    return getEditorPane().getEditor();
896
  }
897
898
  public FileEditorTab getFileEditorTab() {
899
    return this.tab;
900
  }
901
902
  public void setFileEditorTab( final FileEditorTab editorTab ) {
903
    this.tab = editorTab;
904
  }
905
906
  private DefinitionPane getDefinitionPane() {
907
    return this.definitionPane;
908
  }
909
910
  private void setDefinitionPane( final DefinitionPane definitionPane ) {
911
    this.definitionPane = definitionPane;
912
  }
913
914
  private IndexRange getSelectionRange() {
915
    return getEditor().getSelection();
916
  }
917
918
  /**
919
   * Don't look ahead too far when trying to find the end of a node.
920
   *
921
   * @return 512 by default.
922
   */
923
  private int getMaxVarLength() {
924
    return getSettings().getSetting(
925
        "editor.variable.maxLength", DEFAULT_MAX_VAR_LENGTH );
926
  }
927
928
  private Settings getSettings() {
929
    return this.settings;
930
  }
931
932
  private synchronized EventHandler<MouseEvent> getPanelEventHandler() {
933
    if( this.panelEventHandler == null ) {
934
      this.panelEventHandler = createPanelEventHandler();
935
    }
936
937
    return this.panelEventHandler;
938
  }
939
940
  private EventHandler<MouseEvent> createPanelEventHandler() {
941
    return new PanelEventHandler();
942
  }
943
944
  /**
945
   * Responsible for handling double-click events on the definition pane.
946
   */
947
  private class PanelEventHandler implements EventHandler<MouseEvent> {
948
949
    public PanelEventHandler() {
950
    }
951
952
    @Override
953
    public void handle( final MouseEvent event ) {
954
      final Object source = event.getSource();
955
956
      if( source instanceof TreeView ) {
957
        final TreeView<?> tree = (TreeView<?>) source;
958
        final TreeItem<?> item = tree.getSelectionModel().getSelectedItem();
959
960
        if( item instanceof VariableTreeItem ) {
961
          final VariableTreeItem<?> var = (VariableTreeItem<?>) item;
962
          final String text = decorate( var.toPath() );
963
964
          replaceSelection( text );
965
        }
966
      }
967
    }
968
  }
969
}
1970
A src/main/java/com/scrivenvar/editors/markdown/HyperlinkModel.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
32
/**
33
 * Represents the model for a hyperlink: text and url text.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public class HyperlinkModel {
38
39
  private String text;
40
  private String url;
41
  private String title;
42
43
  /**
44
   * Constructs a new hyperlink model in Markdown format by default with no
45
   * title (i.e., tooltip).
46
   *
47
   * @param text The hyperlink text displayed (e.g., displayed to the user).
48
   * @param url The destination URL (e.g., when clicked).
49
   */
50
  public HyperlinkModel( final String text, final String url ) {
51
    this( text, url, null );
52
  }
53
54
  /**
55
   * Constructs a new hyperlink model for the given AST link.
56
   * 
57
   * @param link A markdown link.
58
   */
59
  public HyperlinkModel( final Link link ) {
60
    this(
61
      link.getText().toString(),
62
      link.getUrl().toString(),
63
      link.getTitle().toString()
64
    );
65
  }
66
67
  /**
68
   * Constructs a new hyperlink model in Markdown format by default.
69
   *
70
   * @param text The hyperlink text displayed (e.g., displayed to the user).
71
   * @param url The destination URL (e.g., when clicked).
72
   * @param title The hyperlink title (e.g., shown as a tooltip).
73
   */
74
  public HyperlinkModel( final String text, final String url, final String title ) {
75
    setText( text );
76
    setUrl( url );
77
    setTitle( title );
78
  }
79
80
  /**
81
   * Returns the string in Markdown format by default.
82
   *
83
   * @return A markdown version of the hyperlink.
84
   */
85
  @Override
86
  public String toString() {
87
    String format = "%s%s%s";
88
89
    if( hasText() ) {
90
      format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
91
    }
92
93
    // Becomes ""+URL+"" if no text is set.
94
    // Becomes [TITLE]+(URL)+"" if no title is set.
95
    // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
96
    return String.format( format, getText(), getUrl(), getTitle() );
97
  }
98
99
  public final void setText( final String text ) {
100
    this.text = nullSafe( text );
101
  }
102
103
  public final void setUrl( final String url ) {
104
    this.url = nullSafe( url );
105
  }
106
107
  public final void setTitle( final String title ) {
108
    this.title = nullSafe( title );
109
  }
110
111
  /**
112
   * Answers whether text has been set for the hyperlink.
113
   *
114
   * @return true This is a text link.
115
   */
116
  public boolean hasText() {
117
    return !getText().isEmpty();
118
  }
119
120
  /**
121
   * Answers whether a title (tooltip) has been set for the hyperlink.
122
   *
123
   * @return true There is a title.
124
   */
125
  public boolean hasTitle() {
126
    return !getTitle().isEmpty();
127
  }
128
129
  public String getText() {
130
    return this.text;
131
  }
132
133
  public String getUrl() {
134
    return this.url;
135
  }
136
137
  public String getTitle() {
138
    return this.title;
139
  }
140
141
  private String nullSafe( final String s ) {
142
    return s == null ? "" : s;
143
  }
144
}
1145
A src/main/java/com/scrivenvar/editors/markdown/LinkVisitor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
import com.vladsch.flexmark.util.ast.Node;
32
import com.vladsch.flexmark.util.ast.NodeVisitor;
33
import com.vladsch.flexmark.util.ast.VisitHandler;
34
35
/**
36
 * @author White Magic Software, Ltd.
37
 */
38
public class LinkVisitor {
39
40
  private NodeVisitor visitor;
41
  private Link link;
42
  private final int offset;
43
44
  /**
45
   * Creates a hyperlink given an offset into a paragraph and the markdown AST
46
   * link node.
47
   *
48
   * @param index Index into the paragraph that indicates the hyperlink to
49
   *              change.
50
   */
51
  public LinkVisitor( final int index ) {
52
    this.offset = index;
53
  }
54
55
  public Link process( final Node root ) {
56
    getVisitor().visit( root );
57
    return getLink();
58
  }
59
60
  /**
61
   * @param link Not null.
62
   */
63
  private void visit( final Link link ) {
64
    final int began = link.getStartOffset();
65
    final int ended = link.getEndOffset();
66
    final int index = getOffset();
67
68
    if( index >= began && index <= ended ) {
69
      setLink( link );
70
    }
71
  }
72
73
  private synchronized NodeVisitor getVisitor() {
74
    if( this.visitor == null ) {
75
      this.visitor = createVisitor();
76
    }
77
78
    return this.visitor;
79
  }
80
81
  protected NodeVisitor createVisitor() {
82
    return new NodeVisitor(
83
        new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
84
  }
85
86
  private Link getLink() {
87
    return this.link;
88
  }
89
90
  private void setLink( final Link link ) {
91
    this.link = link;
92
  }
93
94
  public int getOffset() {
95
    return this.offset;
96
  }
97
}
198
A src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import com.scrivenvar.dialogs.ImageDialog;
31
import com.scrivenvar.dialogs.LinkDialog;
32
import com.scrivenvar.editors.EditorPane;
33
import com.scrivenvar.processors.MarkdownProcessor;
34
import com.vladsch.flexmark.ast.Link;
35
import com.vladsch.flexmark.util.ast.Node;
36
import javafx.scene.control.Dialog;
37
import javafx.scene.control.IndexRange;
38
import javafx.scene.input.KeyEvent;
39
import javafx.stage.Window;
40
import org.fxmisc.richtext.StyleClassedTextArea;
41
42
import java.nio.file.Path;
43
import java.util.regex.Matcher;
44
import java.util.regex.Pattern;
45
46
import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN;
47
import static com.scrivenvar.util.Utils.ltrim;
48
import static com.scrivenvar.util.Utils.rtrim;
49
import static javafx.scene.input.KeyCode.ENTER;
50
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
51
52
/**
53
 * Markdown editor pane.
54
 *
55
 * @author Karl Tauber and White Magic Software, Ltd.
56
 */
57
public class MarkdownEditorPane extends EditorPane {
58
59
  private static final Pattern AUTO_INDENT_PATTERN = Pattern.compile(
60
      "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
61
62
  public MarkdownEditorPane() {
63
    initEditor();
64
  }
65
66
  private void initEditor() {
67
    final StyleClassedTextArea textArea = getEditor();
68
69
    textArea.setWrapText( true );
70
    textArea.getStyleClass().add( "markdown-editor" );
71
    textArea.getStylesheets().add( STYLESHEET_MARKDOWN );
72
73
    addKeyboardListener( keyPressed( ENTER ), this::enterPressed );
74
  }
75
76
  private void enterPressed( final KeyEvent e ) {
77
    final StyleClassedTextArea textArea = getEditor();
78
    final String currentLine =
79
        textArea.getText( textArea.getCurrentParagraph() );
80
    final Matcher matcher = AUTO_INDENT_PATTERN.matcher( currentLine );
81
82
    String newText = "\n";
83
84
    if( matcher.matches() ) {
85
      if( !matcher.group( 2 ).isEmpty() ) {
86
        // indent new line with same whitespace characters and list markers
87
        // as current line
88
        newText = newText.concat( matcher.group( 1 ) );
89
      }
90
      else {
91
        // current line contains only whitespace characters and list markers
92
        // --> empty current line
93
        final int caretPosition = textArea.getCaretPosition();
94
        textArea.selectRange( caretPosition - currentLine.length(),
95
                              caretPosition );
96
      }
97
    }
98
99
    textArea.replaceSelection( newText );
100
101
    // Ensure that the window scrolls when Enter is pressed at the bottom of
102
    // the pane.
103
    textArea.requestFollowCaret();
104
  }
105
106
  public void surroundSelection( final String leading, final String trailing ) {
107
    surroundSelection( leading, trailing, null );
108
  }
109
110
  public void surroundSelection( String leading, String trailing,
111
                                 final String hint ) {
112
    final StyleClassedTextArea textArea = getEditor();
113
114
    // Note: not using textArea.insertText() to insert leading and trailing
115
    // because this would add two changes to undo history
116
    final IndexRange selection = textArea.getSelection();
117
    int start = selection.getStart();
118
    int end = selection.getEnd();
119
120
    final String selectedText = textArea.getSelectedText();
121
122
    // remove leading and trailing whitespaces from selected text
123
    String trimmedSelectedText = selectedText.trim();
124
    if( trimmedSelectedText.length() < selectedText.length() ) {
125
      start += selectedText.indexOf( trimmedSelectedText );
126
      end = start + trimmedSelectedText.length();
127
    }
128
129
    // remove leading whitespaces from leading text if selection starts at zero
130
    if( start == 0 ) {
131
      leading = ltrim( leading );
132
    }
133
134
    // remove trailing whitespaces from trailing text if selection ends at
135
    // text end
136
    if( end == textArea.getLength() ) {
137
      trailing = rtrim( trailing );
138
    }
139
140
    // remove leading line separators from leading text
141
    // if there are line separators before the selected text
142
    if( leading.startsWith( "\n" ) ) {
143
      for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
144
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
145
          break;
146
        }
147
        leading = leading.substring( 1 );
148
      }
149
    }
150
151
    // remove trailing line separators from trailing or leading text
152
    // if there are line separators after the selected text
153
    final boolean trailingIsEmpty = trailing.isEmpty();
154
    String str = trailingIsEmpty ? leading : trailing;
155
156
    if( str.endsWith( "\n" ) ) {
157
      final int length = textArea.getLength();
158
159
      for( int i = end; i < length && str.endsWith( "\n" ); i++ ) {
160
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
161
          break;
162
        }
163
164
        str = str.substring( 0, str.length() - 1 );
165
      }
166
167
      if( trailingIsEmpty ) {
168
        leading = str;
169
      }
170
      else {
171
        trailing = str;
172
      }
173
    }
174
175
    int selStart = start + leading.length();
176
    int selEnd = end + leading.length();
177
178
    // insert hint text if selection is empty
179
    if( hint != null && trimmedSelectedText.isEmpty() ) {
180
      trimmedSelectedText = hint;
181
      selEnd = selStart + hint.length();
182
    }
183
184
    // prevent undo merging with previous text entered by user
185
    getUndoManager().preventMerge();
186
187
    // replace text and update selection
188
    textArea.replaceText( start,
189
                          end,
190
                          leading + trimmedSelectedText + trailing );
191
    textArea.selectRange( selStart, selEnd );
192
  }
193
194
  /**
195
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
196
   * the markdown AST.
197
   *
198
   * @return An instance containing the link URL and display text.
199
   */
200
  private HyperlinkModel getHyperlink() {
201
    final StyleClassedTextArea textArea = getEditor();
202
    final String selectedText = textArea.getSelectedText();
203
204
    // Get the current paragraph, convert to Markdown nodes.
205
    final MarkdownProcessor mp = new MarkdownProcessor( null );
206
    final int p = textArea.getCurrentParagraph();
207
    final String paragraph = textArea.getText( p );
208
    final Node node = mp.toNode( paragraph );
209
    final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() );
210
    final Link link = visitor.process( node );
211
212
    if( link != null ) {
213
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
214
    }
215
216
    return createHyperlinkModel(
217
        link, selectedText, "https://localhost"
218
    );
219
  }
220
221
  @SuppressWarnings("SameParameterValue")
222
  private HyperlinkModel createHyperlinkModel(
223
      final Link link, final String selection, final String url ) {
224
225
    return link == null
226
        ? new HyperlinkModel( selection, url )
227
        : new HyperlinkModel( link );
228
  }
229
230
  private Path getParentPath() {
231
    final Path parentPath = getPath();
232
    return (parentPath != null) ? parentPath.getParent() : null;
233
  }
234
235
  private Dialog<String> createLinkDialog() {
236
    return new LinkDialog( getWindow(), getHyperlink() );
237
  }
238
239
  private Dialog<String> createImageDialog() {
240
    return new ImageDialog( getWindow(), getParentPath() );
241
  }
242
243
  private void insertObject( final Dialog<String> dialog ) {
244
    dialog.showAndWait().ifPresent(
245
        result -> getEditor().replaceSelection( result )
246
    );
247
  }
248
249
  public void insertLink() {
250
    insertObject( createLinkDialog() );
251
  }
252
253
  public void insertImage() {
254
    insertObject( createImageDialog() );
255
  }
256
257
  private Window getWindow() {
258
    return getScrollPane().getScene().getWindow();
259
  }
260
}
1261
A src/main/java/com/scrivenvar/predicates/files/FileTypePredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.files;
29
30
import java.io.File;
31
import java.nio.file.FileSystems;
32
import java.nio.file.PathMatcher;
33
import java.util.Collection;
34
import java.util.function.Predicate;
35
36
/**
37
 * Responsible for testing whether a given path (to a file) matches one of the
38
 * filename extension patterns provided during construction.
39
 *
40
 * @author White Magic Software, Ltd.
41
 */
42
public class FileTypePredicate implements Predicate<File> {
43
44
  private final PathMatcher matcher;
45
46
  /**
47
   * Constructs a new instance given a set of file extension globs.
48
   *
49
   * @param patterns Comma-separated list of globbed extensions including the
50
   * Kleene star (e.g., <code>*.md,*.markdown,*.txt</code>).
51
   */
52
  public FileTypePredicate( final String patterns ) {
53
    this.matcher = FileSystems.getDefault().getPathMatcher(
54
      "glob:**/{" + patterns + "}"
55
    );
56
  }
57
58
  /**
59
   * Constructs a new instance given a list of file extension globs, each must
60
   * include the Kleene star (a.k.a. asterisk).
61
   *
62
   * @param patterns Collection of globbed extensions.
63
   */
64
  public FileTypePredicate( final Collection<String> patterns ) {
65
    this( String.join( ",", patterns ) );
66
  }
67
68
  /**
69
   * Returns true if the file matches the patterns defined during construction.
70
   *
71
   * @param file The filename to match against the given glob patterns.
72
   *
73
   * @return false The filename does not match the glob patterns.
74
   */
75
  @Override
76
  public boolean test( final File file ) {
77
    return getMatcher().matches( file.toPath() );
78
  }
79
80
  private PathMatcher getMatcher() {
81
    return this.matcher;
82
  }
83
}
184
A src/main/java/com/scrivenvar/predicates/strings/ContainsPredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.strings;
29
30
/**
31
 * Determines if one string contains another.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class ContainsPredicate extends StringPredicate {
36
37
  /**
38
   * Calls the superclass to construct the instance.
39
   *
40
   * @param comparate Not null.
41
   */
42
  public ContainsPredicate( final String comparate ) {
43
    super( comparate );
44
  }
45
46
  /**
47
   * Answers whether the given strings match each other. What match means will
48
   * depend on user preferences. The empty condition is required to return the
49
   * first node in a list of child nodes when the user has not yet selected a
50
   * node.
51
   *
52
   * @param comparator The string to compare against the comparate.
53
   *
54
   * @return true if s1 and s2 are a match according to some criteria,or s2 is
55
   * empty.
56
   */
57
  @Override
58
  public boolean test( final String comparator ) {
59
    final String comparate = getComparate().toLowerCase();
60
    return comparator.contains( comparate.toLowerCase() )
61
      || comparate.isEmpty();
62
  }
63
}
164
A src/main/java/com/scrivenvar/predicates/strings/StartsPredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.strings;
29
30
/**
31
 * Determines if a string starts with another.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class StartsPredicate extends StringPredicate {
36
37
  /**
38
   * Constructs a new instance using a comparate that will be compared with
39
   * the comparator during the test.
40
   *
41
   * @param comparate The string to compare against the comparator.
42
   */
43
  public StartsPredicate( final String comparate ) {
44
    super( comparate );
45
  }
46
47
  /**
48
   * Compares two strings.
49
   *
50
   * @param comparator A non-null string, possibly empty.
51
   *
52
   * @return true The comparator starts with the comparate, ignoring case.
53
   */
54
  @Override
55
  public boolean test( final String comparator ) {
56
    final String comparate = getComparate().toLowerCase();
57
    return comparator.startsWith( comparate.toLowerCase() );
58
  }
59
}
160
A src/main/java/com/scrivenvar/predicates/strings/StringPredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.strings;
29
30
import java.util.function.Predicate;
31
32
/**
33
 * General predicate for different types of string comparisons.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public abstract class StringPredicate implements Predicate<String> {
38
39
  private final String comparate;
40
41
  public StringPredicate( final String comparate ) {
42
    this.comparate = comparate;
43
  }
44
45
  protected String getComparate() {
46
    return this.comparate;
47
  }
48
}
149
A src/main/java/com/scrivenvar/preferences/FilePreferences.java
1
/*
2
 * Copyright 2016 David Croft and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preferences;
29
30
import java.io.File;
31
import java.io.FileInputStream;
32
import java.io.FileOutputStream;
33
import java.io.IOException;
34
import java.util.ArrayList;
35
import java.util.Enumeration;
36
import java.util.List;
37
import java.util.Map;
38
import java.util.Properties;
39
import java.util.TreeMap;
40
import java.util.prefs.AbstractPreferences;
41
import java.util.prefs.BackingStoreException;
42
43
/**
44
 * Preferences implementation that stores to a user-defined file. Local file
45
 * storage is preferred over a certain operating system's monolithic trash heap
46
 * called a registry. When the OS is locked down, the default Preferences
47
 * implementation will try to write to the registry and fail due to permissions
48
 * problems. This class sidesteps the issue entirely by writing to the user's
49
 * home directory, where permissions should be a bit more lax.
50
 */
51
public class FilePreferences extends AbstractPreferences {
52
53
  private final Map<String, String> mRoot = new TreeMap<>();
54
  private final Map<String, FilePreferences> mChildren = new TreeMap<>();
55
  private boolean mRemoved;
56
57
  public FilePreferences( final AbstractPreferences parent, final String name ) {
58
    super( parent, name );
59
60
    try {
61
      sync();
62
    } catch( final BackingStoreException ex ) {
63
      error( ex );
64
    }
65
  }
66
67
  @Override
68
  protected void putSpi( final String key, final String value ) {
69
    mRoot.put( key, value );
70
71
    try {
72
      flush();
73
    } catch( final BackingStoreException ex ) {
74
      error( ex );
75
    }
76
  }
77
78
  @Override
79
  protected String getSpi( final String key ) {
80
    return mRoot.get( key );
81
  }
82
83
  @Override
84
  protected void removeSpi( final String key ) {
85
    mRoot.remove( key );
86
87
    try {
88
      flush();
89
    } catch( final BackingStoreException ex ) {
90
      error( ex );
91
    }
92
  }
93
94
  @Override
95
  protected void removeNodeSpi() throws BackingStoreException {
96
    mRemoved = true;
97
    flush();
98
  }
99
100
  @Override
101
  protected String[] keysSpi() {
102
    return mRoot.keySet().toArray( new String[ 0 ] );
103
  }
104
105
  @Override
106
  protected String[] childrenNamesSpi() {
107
    return mChildren.keySet().toArray( new String[ 0 ] );
108
  }
109
110
  @Override
111
  protected FilePreferences childSpi( final String name ) {
112
    FilePreferences child = mChildren.get( name );
113
114
    if( child == null || child.isRemoved() ) {
115
      child = new FilePreferences( this, name );
116
      mChildren.put( name, child );
117
    }
118
119
    return child;
120
  }
121
122
  @Override
123
  protected void syncSpi() {
124
    if( isRemoved() ) {
125
      return;
126
    }
127
128
    final File file = FilePreferencesFactory.getPreferencesFile();
129
130
    if( !file.exists() ) {
131
      return;
132
    }
133
134
    synchronized( file ) {
135
      final Properties p = new Properties();
136
137
      try {
138
        p.load( new FileInputStream( file ) );
139
140
        final String path = getPath();
141
        final Enumeration<?> pnen = p.propertyNames();
142
143
        while( pnen.hasMoreElements() ) {
144
          final String propKey = (String)pnen.nextElement();
145
146
          if( propKey.startsWith( path ) ) {
147
            final String subKey = propKey.substring( path.length() );
148
149
            // Only load immediate descendants
150
            if( subKey.indexOf( '.' ) == -1 ) {
151
              mRoot.put( subKey, p.getProperty( propKey ) );
152
            }
153
          }
154
        }
155
      } catch( final Exception ex ) {
156
        error( new BackingStoreException( ex ) );
157
      }
158
    }
159
  }
160
161
  private String getPath() {
162
    final FilePreferences parent = (FilePreferences)parent();
163
164
    return parent == null ? "" : parent.getPath() + name() + '.';
165
  }
166
167
  @Override
168
  protected void flushSpi() {
169
    final File file = FilePreferencesFactory.getPreferencesFile();
170
171
    synchronized( file ) {
172
      final Properties p = new Properties();
173
174
      try {
175
        final String path = getPath();
176
177
        if( file.exists() ) {
178
          p.load( new FileInputStream( file ) );
179
180
          final List<String> toRemove = new ArrayList<>();
181
182
          // Make a list of all direct children of this node to be removed
183
          final Enumeration<?> pnen = p.propertyNames();
184
185
          while( pnen.hasMoreElements() ) {
186
            String propKey = (String)pnen.nextElement();
187
            if( propKey.startsWith( path ) ) {
188
              final String subKey = propKey.substring( path.length() );
189
190
              // Only do immediate descendants
191
              if( subKey.indexOf( '.' ) == -1 ) {
192
                toRemove.add( propKey );
193
              }
194
            }
195
          }
196
197
          // Remove them now that the enumeration is done with
198
          for( final String propKey : toRemove ) {
199
            p.remove( propKey );
200
          }
201
        }
202
203
        // If this node hasn't been removed, add back in any values
204
        if( !mRemoved ) {
205
          for( final String s : mRoot.keySet() ) {
206
            p.setProperty( path + s, mRoot.get( s ) );
207
          }
208
        }
209
210
        p.store( new FileOutputStream( file ), "FilePreferences" );
211
      } catch( final Exception ex ) {
212
        error( new BackingStoreException( ex ) );
213
      }
214
    }
215
  }
216
217
  private void error( final BackingStoreException ex ) {
218
    throw new RuntimeException( ex );
219
  }
220
}
1221
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/preview/HTMLPreviewPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import static com.scrivenvar.Constants.CARET_POSITION_BASE;
31
import static com.scrivenvar.Constants.STYLESHEET_PREVIEW;
32
import java.nio.file.Path;
33
import javafx.beans.value.ObservableValue;
34
import javafx.concurrent.Worker.State;
35
import static javafx.concurrent.Worker.State.SUCCEEDED;
36
import javafx.scene.Node;
37
import javafx.scene.layout.Pane;
38
import javafx.scene.web.WebEngine;
39
import javafx.scene.web.WebView;
40
41
/**
42
 * HTML preview pane is responsible for rendering an HTML document.
43
 *
44
 * @author Karl Tauber and White Magic Software, Ltd.
45
 */
46
public final class HTMLPreviewPane extends Pane {
47
48
  private final WebView webView = new WebView();
49
  private Path path;
50
51
  /**
52
   * Creates a new preview pane that can scroll to the caret position within the
53
   * document.
54
   */
55
  public HTMLPreviewPane() {
56
    initListeners();
57
    initTraversal();
58
  }
59
60
  /**
61
   * Initializes observers for document changes. When the document is reloaded
62
   * with new HTML, this triggers a scroll event that repositions the document
63
   * to the injected caret (that corresponds with the position in the text
64
   * editor).
65
   */
66
  private void initListeners() {
67
    // Scrolls to the caret after the content has been loaded.
68
    getEngine().getLoadWorker().stateProperty().addListener(
69
      (ObservableValue<? extends State> observable,
70
        final State oldValue, final State newValue) -> {
71
        if( newValue == SUCCEEDED ) {
72
          scrollToCaret();
73
        }
74
      } );
75
  }
76
77
  /**
78
   * Ensures images can be found relative to the document.
79
   *
80
   * @return The base path element to use for the document, or the empty string
81
   * if no path has been set, yet.
82
   */
83
  private String getBase() {
84
    final Path basePath = getPath();
85
86
    return basePath == null
87
      ? ""
88
      : ("<base href='" + basePath.getParent().toUri().toString() + "'>");
89
  }
90
91
  /**
92
   * Updates the internal HTML source, loads it into the preview pane, then
93
   * scrolls to the caret position.
94
   *
95
   * @param html The new HTML document to display.
96
   */
97
  public void update( final String html ) {
98
    getEngine().loadContent(
99
      "<!DOCTYPE html>"
100
      + "<html>"
101
      + "<head>"
102
      + "<link rel='stylesheet' href='" + getClass().getResource( STYLESHEET_PREVIEW ) + "'>"
103
      + getBase()
104
      + "</head>"
105
      + "<body>"
106
      + html
107
      + "</body>"
108
      + "</html>" );
109
  }
110
111
  /**
112
   * Clears out the HTML content from the preview.
113
   */
114
  public void clear() {
115
    update( "" );
116
  }
117
118
  /**
119
   * Scrolls to the caret position in the document.
120
   */
121
  private void scrollToCaret() {
122
    execute( getScrollScript() );
123
  }
124
  
125
  /**
126
   * Returns the JavaScript used to scroll the WebView pane.
127
   *
128
   * @return A script that tries to center the view port on the CARET POSITION.
129
   */
130
  private String getScrollScript() {
131
    return ""
132
      + "var e = document.getElementById('" + CARET_POSITION_BASE + "');"
133
      + "if( e != null ) { "
134
      + "  Element.prototype.topOffset = function () {"
135
      + "    return this.offsetTop + (this.offsetParent ? this.offsetParent.topOffset() : 0);"
136
      + "  };"
137
      + "  window.scrollTo( 0, e.topOffset() - (window.innerHeight / 2 ) );"
138
      + "}";
139
  }
140
141
  /**
142
   * Prevent tabbing into the preview pane.
143
   */
144
  private void initTraversal() {
145
    getWebView().setFocusTraversable( false );
146
  }
147
148
  private void execute( final String script ) {
149
    getEngine().executeScript( script );
150
  }
151
152
  private WebEngine getEngine() {
153
    return getWebView().getEngine();
154
  }
155
156
  private WebView getWebView() {
157
    return this.webView;
158
  }
159
160
  private Path getPath() {
161
    return this.path;
162
  }
163
164
  public void setPath( final Path path ) {
165
    this.path = path;
166
  }
167
168
  /**
169
   * Content to embed in a panel.
170
   *
171
   * @return The content to display to the user.
172
   */
173
  public Node getNode() {
174
    return getWebView();
175
  }
176
}
1177
A src/main/java/com/scrivenvar/processors/AbstractProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
/**
31
 * Responsible for transforming a document through a variety of chained
32
 * handlers. If there are conditions where this handler should not process the
33
 * entire chain, create a second handler, or split the chain into reusable
34
 * sub-chains.
35
 *
36
 * @author White Magic Software, Ltd.
37
 * @param <T> The type of object to process.
38
 */
39
public abstract class AbstractProcessor<T> implements Processor<T> {
40
41
  protected static final char NEWLINE = '\n';
42
43
  /**
44
   * When performing string searches using indexOf, a return value of -1
45
   * indicates that the string could not be found.
46
   */
47
  protected static final int INDEX_NOT_FOUND = -1;
48
49
  /**
50
   * Used while processing the entire chain; null to signify no more links.
51
   */
52
  private final Processor<T> next;
53
54
  /**
55
   * Constructs a succession without a successor (i.e., next is null).
56
   */
57
  protected AbstractProcessor() {
58
    this( null );
59
  }
60
61
  /**
62
   * Constructs a new default handler with a given successor.
63
   *
64
   * @param successor Use null to indicate last link in the chain.
65
   */
66
  public AbstractProcessor( final Processor<T> successor ) {
67
    this.next = successor;
68
  }
69
70
  /**
71
   * Processes links in the chain while there are successors and valid data to
72
   * process.
73
   *
74
   * @param t The object to process.
75
   */
76
  @Override
77
  public synchronized void processChain( T t ) {
78
    Processor<T> handler = this;
79
80
    while( handler != null && t != null ) {
81
      t = handler.processLink( t );
82
      handler = handler.next();
83
    }
84
  }
85
86
  @Override
87
  public Processor<T> next() {
88
    return this.next;
89
  }
90
}
191
A src/main/java/com/scrivenvar/processors/CaretInsertionProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static com.scrivenvar.Constants.CARET_POSITION_MD;
31
import javafx.beans.property.IntegerProperty;
32
import javafx.beans.property.SimpleIntegerProperty;
33
import javafx.beans.value.ObservableValue;
34
35
/**
36
 * Base class for inserting the magic CARET POSITION into the text so that, upon
37
 * previewing, the preview pane can scroll to the correct position (relative to
38
 * the caret position in the editor).
39
 *
40
 * @author White Magic Software, Ltd.
41
 */
42
public abstract class CaretInsertionProcessor extends AbstractProcessor<String> {
43
44
  private final IntegerProperty caretPosition = new SimpleIntegerProperty();
45
  private final static String NEWLINE_CARET_POSITION_MD = NEWLINE + CARET_POSITION_MD;
46
47
  public CaretInsertionProcessor(
48
    final Processor<String> processor,
49
    final ObservableValue<Integer> position ) {
50
    super( processor );
51
    this.caretPosition.bind( position );
52
  }
53
54
  /**
55
   * Inserts the caret position token into the text at an offset that won't
56
   * interfere with parsing the text itself, regardless of text format.
57
   *
58
   * @param text The text document to change.
59
   * @param i The caret position token insertion point to use, or -1 to return
60
   * the text without any injection.
61
   *
62
   * @return The given text with a caret position token inserted at the given
63
   * offset.
64
   */
65
  protected String inject( final String text, final int i ) {
66
    if( i > 0 && i <= text.length() ) {
67
      // Preserve the newline character when inserting the caret position mark.
68
      final String replacement = text.charAt( i - 1 ) == NEWLINE
69
        ? NEWLINE_CARET_POSITION_MD
70
        : CARET_POSITION_MD;
71
72
      return new StringBuilder( text ).replace( i, i, replacement ).toString();
73
    }
74
75
    return text;
76
  }
77
78
  /**
79
   * Returns true if i is greater than or equal to min and less than or equal to
80
   * max.
81
   *
82
   * @param i The value to check.
83
   * @param min The lower bound.
84
   * @param max The upper bound.
85
   *
86
   * @return false The value of i is either lower than min or greater than max.
87
   */
88
  protected boolean isBetween( int i, int min, int max ) {
89
    return i >= min && i <= max;
90
  }
91
92
  /**
93
   * Returns the editor's caret position.
94
   *
95
   * @return Where the user has positioned the caret.
96
   */
97
  protected int getCaretPosition() {
98
    return this.caretPosition.getValue();
99
  }
100
}
1101
A src/main/java/com/scrivenvar/processors/CaretReplacementProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static com.scrivenvar.Constants.CARET_POSITION_HTML;
31
import static com.scrivenvar.Constants.CARET_POSITION_MD;
32
33
/**
34
 * Responsible for replacing the caret position marker with an HTML element
35
 * suitable to use as a reference for scrolling a view port.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public class CaretReplacementProcessor extends AbstractProcessor<String> {
40
41
  public CaretReplacementProcessor( final Processor<String> processor ) {
42
    super( processor );
43
  }
44
45
  /**
46
   * Replaces each MD_CARET_POSITION with an HTML element that has an id
47
   * attribute of CARET_POSITION. This should only replace one item.
48
   *
49
   * @param t The text that contains
50
   * @return The value of the first instance replaced.
51
   */
52
  @Override
53
  public String processLink( final String t ) {
54
    return replace( t, CARET_POSITION_MD, CARET_POSITION_HTML );
55
  }
56
57
  /**
58
   * Replaces the needle with thread in the given haystack. Based on Apache
59
   * Commons 3 StringUtils.replace method. Should be faster than String.replace,
60
   * which performs a little regex under the hood.
61
   *
62
   * @param haystack Search this string for the needle, must not be null.
63
   * @param needle   The text to find in the haystack.
64
   * @param thread   Replace the needle with this text, if the needle is found.
65
   * @return The haystack with the first instance of needle replaced with
66
   * thread.
67
   */
68
  @SuppressWarnings("SameParameterValue")
69
  private static String replace(
70
      final String haystack, final String needle, final String thread ) {
71
    final int end = haystack.indexOf( needle );
72
73
    return end == INDEX_NOT_FOUND ?
74
        haystack :
75
        haystack.substring( 0, end ) + thread +
76
            haystack.substring( end + needle.length() );
77
  }
78
}
179
A src/main/java/com/scrivenvar/processors/DefaultVariableProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
31
import java.util.Map;
32
33
/**
34
 * Processes variables in the document and inserts their values into the
35
 * post-processed text. The default variable syntax is <code>$variable$</code>.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public class DefaultVariableProcessor extends AbstractProcessor<String> {
40
41
  private Map<String, String> definitions;
42
43
  /**
44
   * Constructs a variable processor to dereference variables.
45
   *
46
   * @param successor Usually the HTML Preview Processor.
47
   */
48
  private DefaultVariableProcessor( final Processor<String> successor ) {
49
    super( successor );
50
  }
51
52
  public DefaultVariableProcessor(
53
    final Processor<String> successor, final Map<String, String> map ) {
54
    this( successor );
55
    setDefinitions( map );
56
  }
57
58
  /**
59
   * Processes the given text document by replacing variables with their values.
60
   *
61
   * @param text The document text that includes variables that should be
62
   * replaced with values when rendered as HTML.
63
   *
64
   * @return The text with all variables replaced.
65
   */
66
  @Override
67
  public String processLink( final String text ) {
68
    return replace( text, getDefinitions() );
69
  }
70
71
  /**
72
   * Returns the map to use for variable substitution.
73
   *
74
   * @return A map of variable names to values.
75
   */
76
  protected Map<String, String> getDefinitions() {
77
    return this.definitions;
78
  }
79
80
  private void setDefinitions( final Map<String, String> definitions ) {
81
    this.definitions = definitions;
82
  }
83
}
184
A src/main/java/com/scrivenvar/processors/HTMLPreviewProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.preview.HTMLPreviewPane;
31
32
/**
33
 * Responsible for notifying the HTMLPreviewPane when the succession chain has
34
 * updated. This decouples knowledge of changes to the editor panel from the
35
 * HTML preview panel as well as any processing that takes place before the
36
 * final HTML preview is rendered. This should be the last link in the processor
37
 * chain.
38
 *
39
 * @author White Magic Software, Ltd.
40
 */
41
public class HTMLPreviewProcessor extends AbstractProcessor<String> {
42
43
  // There is only one preview panel.
44
  private static HTMLPreviewPane sHtmlPreviewPane;
45
46
  /**
47
   * Constructs the end of a processing chain.
48
   *
49
   * @param htmlPreviewPane The pane to update with the post-processed document.
50
   */
51
  public HTMLPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) {
52
    sHtmlPreviewPane = htmlPreviewPane;
53
  }
54
55
  /**
56
   * Update the preview panel using HTML from the succession chain.
57
   *
58
   * @param html The document content to render in the preview pane. The HTML
59
   * should not contain a doctype, head, or body tag, only content to render
60
   * within the body.
61
   *
62
   * @return null
63
   */
64
  @Override
65
  public String processLink( final String html ) {
66
    getHtmlPreviewPane().update( html );
67
68
    // No more processing required.
69
    return null;
70
  }
71
72
  private HTMLPreviewPane getHtmlPreviewPane() {
73
    return sHtmlPreviewPane;
74
  }
75
}
176
A src/main/java/com/scrivenvar/processors/IdentityProcessor.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
/**
31
 * This is the default processor used when an unknown filename extension is
32
 * encountered.
33
 *
34
 * @author White Magic Software, Ltd.
35
 */
36
public class IdentityProcessor extends AbstractProcessor<String> {
37
38
  /**
39
   * Passes the link to the super constructor.
40
   *
41
   * @param link The next processor in the chain to use for text processing.
42
   */
43
  public IdentityProcessor( final Processor<String> link ) {
44
    super( link );
45
  }
46
47
  /**
48
   * Returns the given string, modified with "pre" tags.
49
   *
50
   * @param t The string to return, enclosed in "pre" tags.
51
   * @return The value of t wrapped in "pre" tags.
52
   */
53
  @Override
54
  public String processLink( final String t ) {
55
    return "<pre>" + t + "</pre>";
56
  }
57
}
158
A src/main/java/com/scrivenvar/processors/InlineRProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.service.Options;
32
import com.scrivenvar.service.events.Notifier;
33
import org.renjin.eval.EvalException;
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.Map;
40
41
import static com.scrivenvar.Constants.*;
42
import static com.scrivenvar.Messages.get;
43
import static com.scrivenvar.decorators.RVariableDecorator.PREFIX;
44
import static com.scrivenvar.decorators.RVariableDecorator.SUFFIX;
45
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
46
import static java.lang.Math.min;
47
48
/**
49
 * Transforms a document containing R statements into Markdown.
50
 *
51
 * @author White Magic Software, Ltd.
52
 */
53
public final class InlineRProcessor extends DefaultVariableProcessor {
54
55
  private static final Notifier NOTIFIER = Services.load( Notifier.class );
56
  private static final Options OPTIONS = Services.load( Options.class );
57
58
  // Only one editor is open at a time.
59
  private static final ScriptEngine ENGINE =
60
      (new ScriptEngineManager()).getEngineByName( "Renjin" );
61
62
  /**
63
   * Constructs a processor capable of evaluating R statements.
64
   *
65
   * @param processor Subsequent link in the processing chain.
66
   * @param map       Resolved definitions map.
67
   */
68
  public InlineRProcessor(
69
      final Processor<String> processor,
70
      final Map<String, String> map ) {
71
    super( processor, map );
72
    init();
73
  }
74
75
  /**
76
   * Initialises the R code so that R can find imported libraries.
77
   */
78
  private void init() {
79
    try {
80
      final Path wd = getWorkingDirectory();
81
      final String dir = wd.toString().replace( '\\', '/' );
82
      final Map<String, String> definitions = getDefinitions();
83
      definitions.put( "$application.r.working.directory$", dir );
84
85
      final String initScript = getInitScript();
86
87
      if( !initScript.isEmpty() ) {
88
        final String rScript = replace( initScript, getDefinitions() );
89
        eval( rScript );
90
      }
91
    } catch( final Exception e ) {
92
      // Tell the user that there was a problem.
93
      getNotifier().notify( e.getMessage() );
94
    }
95
  }
96
97
  /**
98
   * Loads the R init script from the application's persisted preferences.
99
   *
100
   * @return A non-null String, possibly empty.
101
   */
102
  private String getInitScript() {
103
    return getOptions().get( PERSIST_R_STARTUP, "" );
104
  }
105
106
  /**
107
   * Evaluates all R statements in the source document and inserts the
108
   * calculated value into the generated document.
109
   *
110
   * @param text The document text that includes variables that should be
111
   *             replaced with values when rendered as HTML.
112
   * @return The generated document with output from all R statements
113
   * substituted with value returned from their execution.
114
   */
115
  @Override
116
  public String processLink( final String text ) {
117
    final int length = text.length();
118
    final int prefixLength = PREFIX.length();
119
120
    // Pre-allocate the same amount of space. A calculation is longer to write
121
    // than its computed value inserted into the text.
122
    final StringBuilder sb = new StringBuilder( length );
123
124
    int prevIndex = 0;
125
    int currIndex = text.indexOf( PREFIX );
126
127
    while( currIndex >= 0 ) {
128
      // Copy everything up to, but not including, an R statement (`r#).
129
      sb.append( text, prevIndex, currIndex );
130
131
      // Jump to the start of the R statement.
132
      prevIndex = currIndex + prefixLength;
133
134
      // Find the statement ending (`), without indexing past the text boundary.
135
      currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
136
137
      // Only evalutate inline R statements that have end delimiters.
138
      if( currIndex > 1 ) {
139
        // Extract the inline R statement to be evaluated.
140
        final String r = text.substring( prevIndex, currIndex );
141
142
        // Pass the R statement into the R engine for evaluation.
143
        try {
144
          final Object result = eval( r );
145
146
          // Append the string representation of the result into the text.
147
          sb.append( result );
148
        } catch( final Exception e ) {
149
          // If the string couldn't be parsed using R, append the statement
150
          // that failed to parse, instead of its evaluated value.
151
          sb.append( PREFIX ).append( r ).append( SUFFIX );
152
153
          // Tell the user that there was a problem.
154
          getNotifier().notify( get( STATUS_PARSE_ERROR,
155
                                     e.getMessage(), currIndex )
156
          );
157
        }
158
159
        // Retain the R statement's ending position in the text.
160
        prevIndex = currIndex + 1;
161
      }
162
163
      // Find the start of the next inline R statement.
164
      currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) );
165
    }
166
167
    // Copy from the previous index to the end of the string.
168
    return sb.append( text.substring( min( prevIndex, length ) ) ).toString();
169
  }
170
171
  /**
172
   * Evaluate an R expression and return the resulting object.
173
   *
174
   * @param r The expression to evaluate.
175
   * @return The object resulting from the evaluation.
176
   */
177
  private Object eval( final String r ) throws ScriptException, EvalException {
178
    return getScriptEngine().eval( r );
179
  }
180
181
  private synchronized ScriptEngine getScriptEngine() {
182
    return ENGINE;
183
  }
184
185
  private Notifier getNotifier() {
186
    return NOTIFIER;
187
  }
188
189
  private Options getOptions() {
190
    return OPTIONS;
191
  }
192
193
  /**
194
   * This will return the given path if not null, otherwise it will return
195
   * the path to the user's directory.
196
   *
197
   * @return A non-null path.
198
   */
199
  private Path getWorkingDirectory() {
200
    return Path.of( getPreference( PERSIST_R_DIRECTORY, USER_DIRECTORY ) );
201
  }
202
203
  /**
204
   * Returns the user-defined preference value for the given key.
205
   *
206
   * @param key          The key to find in the user's preferences.
207
   * @param defaultValue The default value to return if no preference is set.
208
   * @return The value for the preference, or {@code defaultValue} if not found.
209
   */
210
  @SuppressWarnings("SameParameterValue")
211
  private String getPreference( final String key, final String defaultValue ) {
212
    return OPTIONS.get( key, defaultValue );
213
  }
214
}
1215
A src/main/java/com/scrivenvar/processors/MarkdownCaretInsertionProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static java.lang.Character.isLetter;
31
import static java.lang.Math.min;
32
33
import javafx.beans.value.ObservableValue;
34
35
/**
36
 * Responsible for inserting a caret position token into a markdown document.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class MarkdownCaretInsertionProcessor extends CaretInsertionProcessor {
41
42
  /**
43
   * Constructs a processor capable of inserting a caret marker into Markdown.
44
   *
45
   * @param processor The next processor in the chain.
46
   * @param position  The caret's current position in the text.
47
   */
48
  public MarkdownCaretInsertionProcessor(
49
      final Processor<String> processor,
50
      final ObservableValue<Integer> position ) {
51
    super( processor, position );
52
  }
53
54
  /**
55
   * Changes the text to insert a "caret" at the caret position. This will
56
   * insert the unique key of Constants.MD_CARET_POSITION into the document.
57
   *
58
   * @param t The text document to process.
59
   * @return The text with the caret position token inserted at the caret
60
   * position.
61
   */
62
  @Override
63
  public String processLink( final String t ) {
64
    final int length = t.length();
65
    int offset = min( getCaretPosition(), length );
66
67
    // TODO: Ensure that the caret position is outside of an element, 
68
    // so that a caret inserted in the image doesn't corrupt it. Such as:
69
    //
70
    // ![Screenshot](images/scr|eenshot.png)
71
    //
72
    // 1. Scan back to the previous EOL, which will be the MD AST start point.
73
    // 2. Scan forward until EOF or EOL, which will be the MD AST ending point.
74
    // 3. Convert the text between start and end into MD AST.
75
    // 4. Find the nearest text node to the caret.
76
    // 5. Insert the CARET_POSITION_MD value in the text at that offsset.
77
    // Insert the caret at the closest non-Markdown delimiter (i.e., the 
78
    // closest character from the caret position forward).
79
    while( offset < length && !isLetter( t.charAt( offset ) ) ) {
80
      offset++;
81
    }
82
83
    return inject( t, offset );
84
  }
85
}
186
A src/main/java/com/scrivenvar/processors/MarkdownProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
31
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
32
import com.vladsch.flexmark.ext.tables.TablesExtension;
33
import com.vladsch.flexmark.html.HtmlRenderer;
34
import com.vladsch.flexmark.parser.Parser;
35
import com.vladsch.flexmark.util.ast.Node;
36
import com.vladsch.flexmark.util.misc.Extension;
37
38
import java.util.ArrayList;
39
import java.util.Collection;
40
41
/**
42
 * Responsible for parsing a Markdown document and rendering it as HTML.
43
 *
44
 * @author White Magic Software, Ltd.
45
 */
46
public class MarkdownProcessor extends AbstractProcessor<String> {
47
48
  private final static HtmlRenderer RENDERER;
49
  private final static Parser PARSER;
50
51
  static {
52
    final Collection<Extension> extensions = new ArrayList<>();
53
    extensions.add( TablesExtension.create() );
54
    extensions.add( SuperscriptExtension.create() );
55
    extensions.add( StrikethroughSubscriptExtension.create() );
56
57
    RENDERER = HtmlRenderer.builder().extensions( extensions ).build();
58
    PARSER = Parser.builder().extensions( extensions ).build();
59
  }
60
61
  /**
62
   * Constructs a new Markdown processor that can create HTML documents.
63
   *
64
   * @param successor Usually the HTML Preview Processor.
65
   */
66
  public MarkdownProcessor( final Processor<String> successor ) {
67
    super( successor );
68
  }
69
70
  /**
71
   * Converts the given Markdown string into HTML, without the doctype, html,
72
   * head, and body tags.
73
   *
74
   * @param markdown The string to convert from Markdown to HTML.
75
   * @return The HTML representation of the Markdown document.
76
   */
77
  @Override
78
  public String processLink( final String markdown ) {
79
    return toHtml( markdown );
80
  }
81
82
  /**
83
   * Returns the AST in the form of a node for the given markdown document. This
84
   * can be used, for example, to determine if a hyperlink exists inside of a
85
   * paragraph.
86
   *
87
   * @param markdown The markdown to convert into an AST.
88
   * @return The markdown AST for the given text (usually a paragraph).
89
   */
90
  public Node toNode( final String markdown ) {
91
    return parse( markdown );
92
  }
93
94
  /**
95
   * Helper method to create an AST given some markdown.
96
   *
97
   * @param markdown The markdown to parse.
98
   * @return The root node of the markdown tree.
99
   */
100
  private Node parse( final String markdown ) {
101
    return getParser().parse( markdown );
102
  }
103
104
  /**
105
   * Converts a string of markdown into HTML.
106
   *
107
   * @param markdown The markdown text to convert to HTML, must not be null.
108
   * @return The markdown rendered as an HTML document.
109
   */
110
  private String toHtml( final String markdown ) {
111
    return getRenderer().render( parse( markdown ) );
112
  }
113
114
  /**
115
   * Creates the Markdown document processor.
116
   *
117
   * @return A Parser that can build an abstract syntax tree.
118
   */
119
  private Parser getParser() {
120
    return PARSER;
121
  }
122
123
  private HtmlRenderer getRenderer() {
124
    return RENDERER;
125
  }
126
}
1127
A src/main/java/com/scrivenvar/processors/Processor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
/**
31
 * Responsible for processing documents from one known format to another.
32
 *
33
 * @param <T> The type of processor to create.
34
 * @author White Magic Software, Ltd.
35
 */
36
public interface Processor<T> {
37
38
  /**
39
   * Provided so that the chain can be invoked from any link using a given
40
   * value. This should be called automatically by a superclass so that
41
   * the links in the chain need only implement the processLink method.
42
   *
43
   * @param t The value to pass along to each link in the chain.
44
   */
45
  void processChain( T t );
46
47
  /**
48
   * Processes the given content providing a transformation from one document
49
   * format into another. For example, this could convert from XML to text using
50
   * an XSLT processor, or from markdown to HTML.
51
   *
52
   * @param t The type of object to process.
53
   * @return The post-processed document, or null if processing should stop.
54
   */
55
  T processLink( T t );
56
57
  /**
58
   * Adds a document processor to call after this processor finishes processing
59
   * the document given to the process method.
60
   *
61
   * @return The processor that should transform the document after this
62
   * instance has finished processing.
63
   */
64
  Processor<T> next();
65
}
166
A src/main/java/com/scrivenvar/processors/ProcessorFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.FileEditorTab;
32
import com.scrivenvar.preview.HTMLPreviewPane;
33
import javafx.beans.value.ObservableValue;
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
 * @author White Magic Software, Ltd.
43
 */
44
public class ProcessorFactory extends AbstractFileFactory {
45
46
  private final HTMLPreviewPane previewPane;
47
  private final Map<String, String> resolvedMap;
48
49
  private Processor<String> terminalProcessChain;
50
51
  /**
52
   * Constructs a factory with the ability to create processors that can perform
53
   * text and caret processing to generate a final preview.
54
   *
55
   * @param previewPane Where the final output is rendered.
56
   * @param resolvedMap Map of definitions to replace before final render.
57
   */
58
  public ProcessorFactory(
59
      final HTMLPreviewPane previewPane,
60
      final Map<String, String> resolvedMap ) {
61
    this.previewPane = previewPane;
62
    this.resolvedMap = resolvedMap;
63
  }
64
65
  /**
66
   * Creates a processor suitable for parsing and rendering the file opened at
67
   * the given tab.
68
   *
69
   * @param tab The tab containing a text editor, path, and caret position.
70
   * @return A processor that can render the given tab's text.
71
   */
72
  public Processor<String> createProcessor( final FileEditorTab tab ) {
73
    final Path path = tab.getPath();
74
    final Processor<String> processor;
75
76
    switch( lookup( path ) ) {
77
      case RMARKDOWN:
78
        processor = createRProcessor( tab );
79
        break;
80
81
      case SOURCE:
82
        processor = createMarkdownProcessor( tab );
83
        break;
84
85
      case XML:
86
        processor = createXMLProcessor( tab );
87
        break;
88
89
      case RXML:
90
        processor = createRXMLProcessor( tab );
91
        break;
92
93
      default:
94
        processor = createIdentityProcessor();
95
        break;
96
    }
97
98
    return processor;
99
  }
100
101
  /**
102
   * Returns a processor common to all processors: markdown, caret position
103
   * token replacer, and an HTML preview renderer.
104
   *
105
   * @return Processors at the end of the processing chain.
106
   */
107
  private Processor<String> getCommonProcessor() {
108
    if( this.terminalProcessChain == null ) {
109
      this.terminalProcessChain = createCommonProcessor();
110
    }
111
112
    return this.terminalProcessChain;
113
  }
114
115
  /**
116
   * Creates and links the processors at the end of the processing chain.
117
   *
118
   * @return A markdown, caret replacement, and preview pane processor chain.
119
   */
120
  private Processor<String> createCommonProcessor() {
121
    final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
122
    final Processor<String> mcrp = new CaretReplacementProcessor( hpp );
123
124
    return new MarkdownProcessor( mcrp );
125
  }
126
127
  protected Processor<String> createIdentityProcessor() {
128
    final Processor<String> hpp = new HTMLPreviewProcessor( getPreviewPane() );
129
130
    return new IdentityProcessor( hpp );
131
  }
132
133
  protected Processor<String> createMarkdownProcessor(
134
      final FileEditorTab tab ) {
135
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
136
    final Processor<String> tpc = getCommonProcessor();
137
    final Processor<String> cip = createMarkdownInsertionProcessor(
138
        tpc, caret );
139
140
    return new DefaultVariableProcessor( cip, getResolvedMap() );
141
  }
142
143
  protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
144
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
145
    final Processor<String> tpc = getCommonProcessor();
146
    final Processor<String> xmlp = new XMLProcessor( tpc, tab.getPath() );
147
    final Processor<String> dvp = new DefaultVariableProcessor(
148
        xmlp, getResolvedMap() );
149
150
    return createXMLInsertionProcessor( dvp, caret );
151
  }
152
153
  protected Processor<String> createRProcessor( final FileEditorTab tab ) {
154
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
155
    final Processor<String> tpc = getCommonProcessor();
156
    final Processor<String> rp = new InlineRProcessor( tpc, getResolvedMap() );
157
    final Processor<String> rvp = new RVariableProcessor(
158
        rp, getResolvedMap() );
159
160
    return createRInsertionProcessor( rvp, caret );
161
  }
162
163
  protected Processor<String> createRXMLProcessor( final FileEditorTab tab ) {
164
    final ObservableValue<Integer> caret = tab.caretPositionProperty();
165
    final Processor<String> tpc = getCommonProcessor();
166
    final Processor<String> xmlp = new XMLProcessor( tpc, tab.getPath() );
167
    final Processor<String> rp = new InlineRProcessor( xmlp, getResolvedMap() );
168
    final Processor<String> rvp = new RVariableProcessor(
169
        rp, getResolvedMap() );
170
171
    return createXMLInsertionProcessor( rvp, caret );
172
  }
173
174
  private Processor<String> createMarkdownInsertionProcessor(
175
      final Processor<String> tpc, final ObservableValue<Integer> caret ) {
176
    return new MarkdownCaretInsertionProcessor( tpc, caret );
177
  }
178
179
  /**
180
   * Create an insertion processor that is aware of R statements and will insert
181
   * a caret outside of any statement the caret falls within.
182
   *
183
   * @param processor Another link in the processor chain.
184
   * @param caret     The caret insertion point.
185
   * @return A processor that can insert a caret token without disturbing any R
186
   * code.
187
   */
188
  private Processor<String> createRInsertionProcessor(
189
      final Processor<String> processor,
190
      final ObservableValue<Integer> caret ) {
191
    return new RMarkdownCaretInsertionProcessor( processor, caret );
192
  }
193
194
  private Processor<String> createXMLInsertionProcessor(
195
      final Processor<String> tpc, final ObservableValue<Integer> caret ) {
196
    return new XMLCaretInsertionProcessor( tpc, caret );
197
  }
198
199
  private HTMLPreviewPane getPreviewPane() {
200
    return this.previewPane;
201
  }
202
203
  /**
204
   * Returns the variable map of interpolated definitions.
205
   *
206
   * @return A map to help dereference variables.
207
   */
208
  private Map<String, String> getResolvedMap() {
209
    return this.resolvedMap;
210
  }
211
}
1212
A src/main/java/com/scrivenvar/processors/RMarkdownCaretInsertionProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import static com.scrivenvar.decorators.RVariableDecorator.PREFIX;
31
import static com.scrivenvar.decorators.RVariableDecorator.SUFFIX;
32
import static java.lang.Integer.max;
33
34
import javafx.beans.value.ObservableValue;
35
36
/**
37
 * Responsible for inserting a caret position token into an R document.
38
 *
39
 * @author White Magic Software, Ltd.
40
 */
41
public class RMarkdownCaretInsertionProcessor
42
    extends MarkdownCaretInsertionProcessor {
43
44
  /**
45
   * Constructs a processor capable of inserting a caret marker into Markdown.
46
   *
47
   * @param processor The next processor in the chain.
48
   * @param position  The caret's current position in the text.
49
   */
50
  public RMarkdownCaretInsertionProcessor(
51
      final Processor<String> processor,
52
      final ObservableValue<Integer> position ) {
53
    super( processor, position );
54
  }
55
56
  /**
57
   * Changes the text to insert a "caret" at the caret position. This will
58
   * insert the unique key of Constants.MD_CARET_POSITION into the document.
59
   *
60
   * @param text The text document to process.
61
   * @return The text with the caret position token inserted at the caret
62
   * position.
63
   */
64
  @Override
65
  public String processLink( final String text ) {
66
    int offset = getCaretPosition();
67
68
    // Search for inline R code from the start of the caret's paragraph.
69
    // This should be much faster than scanning text from the beginning.
70
    int index = text.lastIndexOf( NEWLINE, offset );
71
72
    if( index == INDEX_NOT_FOUND ) {
73
      index = 0;
74
    }
75
76
    // Scan for an inline R statement, either from the nearest paragraph or
77
    // the beginning of the file, whichever was found first.
78
    index = text.indexOf( PREFIX, index );
79
80
    // If there was no R prefix then insert at the caret's initial offset...
81
    if( index != INDEX_NOT_FOUND ) {
82
      // Otherwise, retain the starting index of the first R statement in the
83
      // paragraph.
84
      int rPrefix = index + 1;
85
86
      // Scan for inline R prefixes until the text is exhausted or indexed
87
      // beyond the caret position.
88
      while( index != INDEX_NOT_FOUND && index < offset ) {
89
        // Set rPrefix to the index that might precede the caret. The + 1 is
90
        // to skip passed the leading backtick in the prefix (`r#).
91
        rPrefix = index + 1;
92
93
        // If there are no more R prefixes, exit the loop and look for a
94
        // suffix starting from the rPrefix position.
95
        index = text.indexOf( PREFIX, rPrefix );
96
      }
97
98
      // Scan from the character after the R prefix up to any R suffix.
99
      final int rSuffix = max( text.indexOf( SUFFIX, rPrefix ), rPrefix );
100
101
      // If the caret falls between the rPrefix and rSuffix, then change the
102
      // insertion point.
103
      final boolean between = isBetween( offset, rPrefix, rSuffix );
104
105
      // Insert the caret marker at the start of the R statement.
106
      if( between ) {
107
        offset = rPrefix - 1;
108
      }
109
    }
110
111
    return inject( text, offset );
112
  }
113
}
1114
A src/main/java/com/scrivenvar/processors/RVariableProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
33
/**
34
 * Converts the keys of the resolved map from default form to R form, then
35
 * performs a substitution on the text. The default R variable syntax is
36
 * <code>v$tree$leaf</code>.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class RVariableProcessor extends DefaultVariableProcessor {
41
42
  public RVariableProcessor(
43
      final Processor<String> rp, final Map<String, String> map ) {
44
    super( rp, map );
45
  }
46
47
  /**
48
   * Returns the R-based version of the interpolated variable definitions.
49
   *
50
   * @return Variable names transmogrified from the default syntax to R syntax.
51
   */
52
  @Override
53
  protected Map<String, String> getDefinitions() {
54
    return toR( super.getDefinitions() );
55
  }
56
57
  /**
58
   * Converts the given map from regular variables to R variables.
59
   *
60
   * @param map Map of variable names to values.
61
   * @return Map of R variables.
62
   */
63
  private Map<String, String> toR( final Map<String, String> map ) {
64
    final Map<String, String> rMap = new HashMap<>( map.size() );
65
66
    for( final String key : map.keySet() ) {
67
      rMap.put( toRKey( key ), toRValue( map.get( key ) ) );
68
    }
69
70
    return rMap;
71
  }
72
73
  /**
74
   * Transforms a variable name from $tree.branch.leaf$ to v$tree$branch$leaf
75
   * form.
76
   *
77
   * @param key The variable name to transform, can be empty but not null.
78
   * @return The transformed variable name.
79
   */
80
  private String toRKey( final String key ) {
81
    // Replace all the periods with dollar symbols.
82
    final StringBuilder sb = new StringBuilder( 'v' + key );
83
    final int length = sb.length();
84
85
    // Replace all periods with dollar symbols. Normally we'd check i >= 0,
86
    // but the prepended 'v' is always going to be a 'v', not a dot.
87
    for( int i = length - 1; i > 0; i-- ) {
88
      if( sb.charAt( i ) == '.' ) {
89
        sb.setCharAt( i, '$' );
90
      }
91
    }
92
93
    // The length is always at least 1 (the 'v'), so bounds aren't broken here.
94
    sb.setLength( length - 1 );
95
96
    return sb.toString();
97
  }
98
99
  private String toRValue( final String value ) {
100
    return '\'' + escape( value, '\'', "\\'" ) + '\'';
101
  }
102
103
  /**
104
   * TODO: Make generic method for replacing text.
105
   *
106
   * @param haystack Search this string for the needle, must not be null.
107
   * @param needle   The character to find in the haystack.
108
   * @param thread   Replace the needle with this text, if the needle is found.
109
   * @return The haystack with the all instances of needle replaced with thread.
110
   */
111
  @SuppressWarnings("SameParameterValue")
112
  private String escape(
113
      final String haystack, final char needle, final String thread ) {
114
    int end = haystack.indexOf( needle );
115
116
    if( end < 0 ) {
117
      return haystack;
118
    }
119
120
    final int length = haystack.length();
121
    int start = 0;
122
123
    // Replace up to 32 occurrences before the string reallocates its buffer.
124
    final StringBuilder sb = new StringBuilder( length + 32 );
125
126
    while( end >= 0 ) {
127
      sb.append( haystack, start, end ).append( thread );
128
      start = end + 1;
129
      end = haystack.indexOf( needle, start );
130
    }
131
132
    return sb.append( haystack.substring( start ) ).toString();
133
  }
134
}
1135
A src/main/java/com/scrivenvar/processors/XMLCaretInsertionProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.ximpleware.VTDException;
31
import com.ximpleware.VTDGen;
32
import static com.ximpleware.VTDGen.TOKEN_CHARACTER_DATA;
33
import com.ximpleware.VTDNav;
34
import static java.nio.charset.StandardCharsets.UTF_8;
35
import java.text.ParseException;
36
import javafx.beans.value.ObservableValue;
37
38
/**
39
 * Inserts a caret position indicator into the document.
40
 *
41
 * @author White Magic Software, Ltd.
42
 */
43
public class XMLCaretInsertionProcessor extends CaretInsertionProcessor {
44
45
  private final static VTDGen PARSER = new VTDGen();
46
47
  /**
48
   * Constructs a processor capable of inserting a caret marker into XML.
49
   *
50
   * @param processor The next processor in the chain.
51
   * @param position The caret's current position in the text, cannot be null.
52
   */
53
  public XMLCaretInsertionProcessor(
54
    final Processor<String> processor,
55
    final ObservableValue<Integer> position ) {
56
    super( processor, position );
57
  }
58
59
  /**
60
   * Inserts a caret at a valid position within the XML document.
61
   *
62
   * @param text The string into which caret position marker text is inserted.
63
   *
64
   * @return The text with a caret position marker included, or the original
65
   * text if no insertion point could be found.
66
   */
67
  @Override
68
  public String processLink( final String text ) {
69
    final int caret = getCaretPosition();
70
    int insertOffset = -1;
71
72
    if( text.length() > 0 ) {
73
      try {
74
        final VTDNav vn = getNavigator( text );
75
        final int tokens = vn.getTokenCount();
76
77
        int currTokenIndex = 0;
78
        int prevTokenIndex = currTokenIndex;
79
        int currOffset = 0;
80
81
        // To find the insertion spot even faster, the algorithm could
82
        // use a binary search or interpolation search algorithm. This
83
        // would reduce the worst-case iterations to O(log n) from O(n).
84
        while( currTokenIndex < tokens ) {
85
          if( vn.getTokenType( currTokenIndex ) == TOKEN_CHARACTER_DATA ) {
86
            final int prevOffset = currOffset;
87
            currOffset = vn.getTokenOffset( currTokenIndex );
88
89
            if( currOffset > caret ) {
90
              final int prevLength = vn.getTokenLength( prevTokenIndex );
91
92
              // If the caret falls within the limits of the previous token,
93
              // theninsert the caret position marker at the caret offset.
94
              if( isBetween( caret, prevOffset, prevOffset + prevLength ) ) {
95
                insertOffset = caret;
96
              } else {
97
                // The caret position is outside the previous token's text
98
                // boundaries, but not inside the current text token. The
99
                // caret should be positioned into the closer text token.
100
                // For now, the cursor is positioned at the start of the
101
                // current text token.
102
                insertOffset = currOffset;
103
              }
104
105
              break;
106
            }
107
108
            prevTokenIndex = currTokenIndex;
109
          }
110
111
          currTokenIndex++;
112
        }
113
114
      } catch( final Exception ex ) {
115
        throw new RuntimeException(
116
          new ParseException( ex.getMessage(), caret )
117
        );
118
      }
119
    }
120
121
    return inject( text, insertOffset );
122
  }
123
124
  /**
125
   * Parses the given XML document and returns a high-performance navigator
126
   * instance for scanning through the XML elements.
127
   *
128
   * @param xml The XML document to parse.
129
   *
130
   * @return A document navigator instance.
131
   */
132
  private VTDNav getNavigator( final String xml ) throws VTDException {
133
    final VTDGen vg = getParser();
134
135
    // XML recommends UTF-8 encoding.
136
    // See: http://stackoverflow.com/a/36696214/59087
137
    //
138
    // The encoding should be derived, not assumed.
139
    vg.setDoc( xml.getBytes( UTF_8 ) );
140
    vg.parse( true );
141
    return vg.getNav();
142
  }
143
144
  private synchronized VTDGen getParser() {
145
    return PARSER;
146
  }
147
}
1148
A src/main/java/com/scrivenvar/processors/XMLProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.service.Snitch;
32
import 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
 *
56
 * <code>xml-stylesheet type="text/xsl" href="markdown.xsl"</code>
57
 * <p>
58
 * The XSL must transform the XML document into Markdown, or another format
59
 * recognized by the next link on the chain.
60
 *
61
 * @author White Magic Software, Ltd.
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 processLink( 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/text/AbstractTextReplacer.java
1
/*
2
 * The MIT License
3
 *
4
 * Copyright 2016 .
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy
7
 * of this software and associated documentation files (the "Software"), to deal
8
 * in the Software without restriction, including without limitation the rights
9
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
 * copies of the Software, and to permit persons to whom the Software is
11
 * furnished to do so, subject to the following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be included in
14
 * all copies or substantial portions of the Software.
15
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
 * THE SOFTWARE.
23
 */
24
package com.scrivenvar.processors.text;
25
26
import java.util.Map;
27
28
/**
29
 * Responsible for common behaviour across all text replacer implementations.
30
 *
31
 * @author White Magic Software, Ltd.
32
 */
33
public abstract class AbstractTextReplacer implements TextReplacer {
34
35
  /**
36
   * Default (empty) constructor.
37
   */
38
  protected AbstractTextReplacer() {
39
  }
40
41
  protected String[] keys( final Map<String, String> map ) {
42
    return map.keySet().toArray( new String[ 0 ] );
43
  }
44
45
  protected String[] values( final Map<String, String> map ) {
46
    return map.values().toArray( new String[ 0 ] );
47
  }
48
}
149
A src/main/java/com/scrivenvar/processors/text/AhoCorasickReplacer.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
import org.ahocorasick.trie.Emit;
32
import org.ahocorasick.trie.Trie.TrieBuilder;
33
import static org.ahocorasick.trie.Trie.builder;
34
35
/**
36
 * Replaces text using an Aho-Corasick algorithm.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class AhoCorasickReplacer extends AbstractTextReplacer {
41
42
  /**
43
   * Default (empty) constructor.
44
   */
45
  protected AhoCorasickReplacer() {
46
  }
47
48
  @Override
49
  public String replace( final String text, final Map<String, String> map ) {
50
    // Create a buffer sufficiently large that re-allocations are minimized.
51
    final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) );
52
53
    // The TrieBuilder should only match whole words and ignore overlaps (there
54
    // shouldn't be any).
55
    final TrieBuilder builder = builder().onlyWholeWords().ignoreOverlaps();
56
57
    for( final String key : keys( map ) ) {
58
      builder.addKeyword( key );
59
    }
60
61
    int index = 0;
62
63
    // Replace all instances with dereferenced variables.
64
    for( final Emit emit : builder.build().parseText( text ) ) {
65
      sb.append( text, index, emit.getStart() );
66
      sb.append( map.get( emit.getKeyword() ) );
67
      index = emit.getEnd() + 1;
68
    }
69
70
    // Add the remainder of the string (contains no more matches).
71
    sb.append( text.substring( index ) );
72
73
    return sb.toString();
74
  }
75
}
176
A src/main/java/com/scrivenvar/processors/text/StringUtilsReplacer.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
import static org.apache.commons.lang3.StringUtils.replaceEach;
33
34
/**
35
 * Replaces text using Apache's StringUtils.replaceEach method.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public class StringUtilsReplacer extends AbstractTextReplacer {
40
41
  /**
42
   * Default (empty) constructor.
43
   */
44
  protected StringUtilsReplacer() {
45
  }
46
47
  @Override
48
  public String replace( final String text, final Map<String, String> map ) {
49
    return replaceEach( text, keys( map ), values( map ) );
50
  }
51
}
152
A src/main/java/com/scrivenvar/processors/text/TextReplacementFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Used to generate a class capable of efficiently replacing variable
34
 * definitions with their values.
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public final class TextReplacementFactory {
39
40
  private final static TextReplacer APACHE = new StringUtilsReplacer();
41
  private final static TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
42
43
  /**
44
   * Returns a text search/replacement instance that is reasonably optimal for
45
   * the given length of text.
46
   *
47
   * @param length The length of text that requires some search and replacing.
48
   *
49
   * @return A class that can search and replace text with utmost expediency.
50
   */
51
  public static TextReplacer getTextReplacer( final int length ) {
52
    // After about 1,500 characters, the StringUtils implementation is less
53
    // performant than the Aho-Corsick implementation.
54
    //
55
    // See http://stackoverflow.com/a/40836618/59087
56
    return length < 1500 ? APACHE : AHO_CORASICK;
57
  }
58
59
  /**
60
   * Convenience method to instantiate a suitable text replacer algorithm and
61
   * perform a replacement using the given map. At this point, the values should
62
   * be already dereferenced and ready to be substituted verbatim; any
63
   * recursively defined values must have been interpolated previously.
64
   *
65
   * @param text The text containing zero or more variables to replace.
66
   * @param map The map of variables to their dereferenced values.
67
   *
68
   * @return The text with all variables replaced.
69
   */
70
  public static String replace(
71
    final String text, final Map<String, String> map ) {
72
    return getTextReplacer( text.length() ).replace( text, map );
73
  }
74
}
175
A src/main/java/com/scrivenvar/processors/text/TextReplacer.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Defines the ability to replace text given a set of keys and values.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public interface TextReplacer {
38
39
  /**
40
   * Searches through the given text for any of the keys given in the map and
41
   * replaces the keys that appear in the text with the key's corresponding
42
   * value.
43
   *
44
   * @param text The text that contains zero or more keys.
45
   * @param map  The set of keys mapped to replacement values.
46
   * @return The given text with all keys replaced with corresponding values.
47
   */
48
  String replace( String text, Map<String, String> map );
49
}
150
A src/main/java/com/scrivenvar/service/Options.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import java.util.prefs.BackingStoreException;
31
import java.util.prefs.Preferences;
32
33
/**
34
 * Responsible for persisting options.
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public interface Options extends Service {
39
40
  Preferences getState();
41
42
  /**
43
   * Stores the key and value into the user preferences to be loaded the next
44
   * time the application is launched.
45
   *
46
   * @param key   Name of the key to persist along with its value.
47
   * @param value Value to associate with the key.
48
   * @throws BackingStoreException Could not persist the change.
49
   */
50
  void put( String key, String value ) throws BackingStoreException;
51
52
  /**
53
   * Retrieves the value for a key in the user preferences.
54
   *
55
   * @param key          Retrieve the value of this key.
56
   * @param defaultValue The value to return in the event that the given key has
57
   *                     no associated value.
58
   * @return The value associated with the key.
59
   */
60
  String get( String key, String defaultValue );
61
62
  /**
63
   * Retrieves the value for a key in the user preferences. This will return
64
   * the empty string if the value cannot be found.
65
   *
66
   * @param key The key to find in the preferences.
67
   * @return A non-null, possibly empty value for the key.
68
   */
69
  String get( String key );
70
}
171
A src/main/java/com/scrivenvar/service/Service.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
/**
31
 * All services inherit from this one.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public interface Service {
36
}
137
A src/main/java/com/scrivenvar/service/Settings.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import java.util.Iterator;
31
import java.util.List;
32
33
/**
34
 * Defines how settings and options can be retrieved.
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public interface Settings extends Service {
39
40
  /**
41
   * Returns a setting property or its default value.
42
   *
43
   * @param property     The property key name to obtain its value.
44
   * @param defaultValue The default value to return iff the property cannot be
45
   *                     found.
46
   * @return The property value for the given property key.
47
   */
48
  String getSetting( String property, String defaultValue );
49
50
  /**
51
   * Returns a setting property or its default value.
52
   *
53
   * @param property     The property key name to obtain its value.
54
   * @param defaultValue The default value to return iff the property cannot be
55
   *                     found.
56
   * @return The property value for the given property key.
57
   */
58
  int getSetting( String property, int defaultValue );
59
60
  /**
61
   * Returns a list of property names that begin with the given prefix. The
62
   * prefix is included in any matching results. This will return keys that
63
   * either match the prefix or start with the prefix followed by a dot ('.').
64
   * For example, a prefix value of <code>the.property.name</code> will likely
65
   * return the expected results, but <code>the.property.name.</code> (note the
66
   * extraneous period) will probably not.
67
   *
68
   * @param prefix The prefix to compare against each property name.
69
   * @return The list of property names that have the given prefix.
70
   */
71
  Iterator<String> getKeys( final String prefix );
72
73
  /**
74
   * Convert the generic list of property objects into strings.
75
   *
76
   * @param property The property value to coerce.
77
   * @param defaults The defaults values to use should the property be unset.
78
   * @return The list of properties coerced from objects to strings.
79
   */
80
  List<String> getStringSettingList( String property, List<String> defaults );
81
82
  /**
83
   * Converts the generic list of property objects into strings.
84
   *
85
   * @param property The property value to coerce.
86
   * @return The list of properties coerced from objects to strings.
87
   */
88
  List<String> getStringSettingList( String property );
89
90
  /**
91
   * Changes key's value. This will clear the old value before setting the
92
   * new value so that the old value is erased, not changed into a list.
93
   *
94
   * @param key   The property key name to obtain its value.
95
   * @param value The new value to set.
96
   */
97
  void putSetting( String key, String value );
98
}
199
A src/main/java/com/scrivenvar/service/Snitch.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import java.io.IOException;
31
import java.nio.file.Path;
32
import java.util.Observer;
33
34
/**
35
 * Listens for changes to file system files and directories.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public interface Snitch extends Service, Runnable {
40
41
  /**
42
   * Adds an observer to the set of observers for this object, provided that it
43
   * is not the same as some observer already in the set. The order in which
44
   * notifications will be delivered to multiple observers is not specified.
45
   *
46
   * @param o The object to receive changed events for when monitored files
47
   *          are changed.
48
   */
49
  void addObserver( Observer o );
50
51
  /**
52
   * Listens for changes to the path. If the path specifies a file, then only
53
   * notifications pertaining to that file are sent. Otherwise, change events
54
   * for the directory that contains the file are sent. This method must allow
55
   * for multiple calls to the same file without incurring additional listeners
56
   * or events.
57
   *
58
   * @param file Send notifications when this file changes, can be null.
59
   * @throws IOException Couldn't create a watcher for the given file.
60
   */
61
  void listen( Path file ) throws IOException;
62
63
  /**
64
   * Removes the given file from the notifications list.
65
   *
66
   * @param file The file to stop monitoring for any changes, can be null.
67
   */
68
  void ignore( Path file );
69
70
  /**
71
   * Stop listening for events.
72
   */
73
  void stop();
74
}
175
A src/main/java/com/scrivenvar/service/events/Notification.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
/**
31
 * Represents a message that contains a title and content.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public interface Notification {
36
37
  /**
38
   * Alert title.
39
   *
40
   * @return A non-null string to use as alert message title.
41
   */
42
  String getTitle();
43
44
  /**
45
   * Alert message content.
46
   *
47
   * @return A non-null string that contains information for the user.
48
   */
49
  String getContent();
50
}
151
A src/main/java/com/scrivenvar/service/events/Notifier.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
import 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
 * @author White Magic Software, Ltd.
44
 */
45
public interface Notifier {
46
47
  ButtonType YES = ButtonType.YES;
48
  ButtonType NO = ButtonType.NO;
49
  ButtonType CANCEL = ButtonType.CANCEL;
50
51
  /**
52
   * Notifies the user of a problem.
53
   *
54
   * @param message The problem description.
55
   */
56
  void notify( final String message );
57
58
  /**
59
   * Notifies the user about the exception.
60
   *
61
   * @param ex The exception containing a message to show to the user.
62
   */
63
  default void notify( final Exception ex ) {
64
    assert ex != null;
65
66
    log( ex );
67
    notify( ex.getMessage() );
68
  }
69
70
  /**
71
   * Writes the exception to a log file. The log file should be written
72
   * in the System's temporary directory.
73
   *
74
   * @param ex The exception to show in the status bar and log to a file.
75
   */
76
  default void log( final Exception ex ) {
77
    try(
78
        final FileWriter fw = new FileWriter( getLogPath(), true );
79
        final PrintWriter pw = new PrintWriter( fw )
80
    ) {
81
      ex.printStackTrace( pw );
82
    } catch( final IOException ioe ) {
83
      // The notify method will display the message on the status
84
      // bar.
85
    }
86
  }
87
88
  /**
89
   * Returns the fully qualified path to the log file to write to when
90
   * an exception occurs.
91
   *
92
   * @return Location of the log file for writing unexpected exceptions.
93
   */
94
  File getLogPath();
95
96
  /**
97
   * Causes any displayed notifications to disappear.
98
   */
99
  void clear();
100
101
  /**
102
   * Constructs a default alert message text for a modal alert dialog.
103
   *
104
   * @param title   The dialog box message title.
105
   * @param message The dialog box message content (needs formatting).
106
   * @param args    The arguments to the message content that must be formatted.
107
   * @return The message suitable for building a modal alert dialog.
108
   */
109
  Notification createNotification(
110
      String title,
111
      String message,
112
      Object... args );
113
114
  /**
115
   * Creates an alert of alert type error with a message showing the cause of
116
   * the error.
117
   *
118
   * @param parent  Dialog box owner (for modal purposes).
119
   * @param message The error message, title, and possibly more details.
120
   * @return A modal alert dialog box ready to display using showAndWait.
121
   */
122
  Alert createError( Window parent, Notification message );
123
124
  /**
125
   * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
126
   *
127
   * @param parent  Dialog box owner (for modal purposes).
128
   * @param message The message, title, and possibly more details.
129
   * @return A modal alert dialog box ready to display using showAndWait.
130
   */
131
  Alert createConfirmation( Window parent, Notification message );
132
133
  /**
134
   * Adds an observer to the list of objects that receive notifications about
135
   * error messages to be presented to the user.
136
   *
137
   * @param observer The observer instance to notify.
138
   */
139
  void addObserver( Observer observer );
140
141
  /**
142
   * Removes an observer from the list of objects that receive notifications
143
   * about error messages to be presented to the user.
144
   *
145
   * @param observer The observer instance to no longer notify.
146
   */
147
  void deleteObserver( Observer observer );
148
}
1149
A src/main/java/com/scrivenvar/service/events/impl/ButtonOrderPane.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.service.Settings;
32
import javafx.scene.Node;
33
import javafx.scene.control.ButtonBar;
34
import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS;
35
import javafx.scene.control.DialogPane;
36
37
/**
38
 * Ensures a consistent button order for alert dialogs across platforms (because
39
 * the default button order on Linux defies all logic).
40
 *
41
 * @author White Magic Software, Ltd.
42
 */
43
public class ButtonOrderPane extends DialogPane {
44
45
  @Override
46
  protected Node createButtonBar() {
47
    final ButtonBar node = (ButtonBar)super.createButtonBar();
48
    node.setButtonOrder( getButtonOrder() );
49
    return node;
50
  }
51
52
  private String getButtonOrder() {
53
    return getSetting( "dialog.alert.button.order.windows", BUTTON_ORDER_WINDOWS );
54
  }
55
56
  @SuppressWarnings("SameParameterValue")
57
  private String getSetting( final String key, final String defaultValue ) {
58
    return getSettings().getSetting( key, defaultValue );
59
  }
60
61
  private Settings getSettings() {
62
    return Services.load( Settings.class );
63
  }
64
}
165
A src/main/java/com/scrivenvar/service/events/impl/DefaultNotification.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.service.events.Notification;
31
32
import java.text.MessageFormat;
33
34
/**
35
 * @author White Magic Software, Ltd.
36
 */
37
public class DefaultNotification implements Notification {
38
39
  private final String title;
40
  private final String content;
41
42
  /**
43
   * Constructs default message text for a notification.
44
   *
45
   * @param title   The message title.
46
   * @param message The message content (needs formatting).
47
   * @param args    The arguments to the message content that must be formatted.
48
   */
49
  public DefaultNotification(
50
      final String title,
51
      final String message,
52
      final Object... args ) {
53
    this.title = title;
54
    this.content = MessageFormat.format( message, args );
55
  }
56
57
  @Override
58
  public String getTitle() {
59
    return this.title;
60
  }
61
62
  @Override
63
  public String getContent() {
64
    return this.content;
65
  }
66
}
167
A src/main/java/com/scrivenvar/service/events/impl/DefaultNotifier.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import 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_DEFAULT;
42
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
43
import static javafx.scene.control.Alert.AlertType.ERROR;
44
45
/**
46
 * Provides the ability to notify the user of problems.
47
 *
48
 * @author White Magic Software, Ltd.
49
 */
50
public final class DefaultNotifier extends Observable implements Notifier {
51
52
  public DefaultNotifier() {
53
  }
54
55
  /**
56
   * Notifies all observer instances of the given message.
57
   *
58
   * @param message The text to display to the user.
59
   */
60
  @Override
61
  public void notify( final String message ) {
62
    if( message != null && !message.isBlank() ) {
63
      setChanged();
64
      notifyObservers( message );
65
    }
66
  }
67
68
  @Override
69
  public void clear() {
70
    notify( STATUS_BAR_DEFAULT );
71
  }
72
73
  /**
74
   * Contains all the information that the user needs to know about a problem.
75
   *
76
   * @param title   The context for the message.
77
   * @param message The message content (formatted with the given args).
78
   * @param args    Parameters for the message content.
79
   * @return A notification instance, never null.
80
   */
81
  @Override
82
  public Notification createNotification(
83
      final String title,
84
      final String message,
85
      final Object... args ) {
86
    return new DefaultNotification( title, message, args );
87
  }
88
89
  private Alert createAlertDialog(
90
      final Window parent,
91
      final AlertType alertType,
92
      final Notification message ) {
93
94
    final Alert alert = new Alert( alertType );
95
96
    alert.setDialogPane( new ButtonOrderPane() );
97
    alert.setTitle( message.getTitle() );
98
    alert.setHeaderText( null );
99
    alert.setContentText( message.getContent() );
100
    alert.initOwner( parent );
101
102
    return alert;
103
  }
104
105
  @Override
106
  public Alert createConfirmation( final Window parent,
107
                                   final Notification message ) {
108
    final Alert alert = createAlertDialog( parent, CONFIRMATION, message );
109
110
    alert.getButtonTypes().setAll( YES, NO, CANCEL );
111
112
    return alert;
113
  }
114
115
  @Override
116
  public Alert createError( final Window parent, final Notification message ) {
117
    return createAlertDialog( parent, ERROR, message );
118
  }
119
120
  @Override
121
  public File getLogPath() {
122
    return Paths.get(
123
        System.getProperty( "java.io.tmpdir" ), APP_TITLE + ".log" ).toFile();
124
  }
125
}
1126
A src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.service.impl;
28
29
import static com.scrivenvar.Constants.PREFS_OPTIONS;
30
import static com.scrivenvar.Constants.PREFS_ROOT;
31
import static com.scrivenvar.Constants.PREFS_STATE;
32
33
import com.scrivenvar.service.Options;
34
35
import java.util.prefs.BackingStoreException;
36
import java.util.prefs.Preferences;
37
38
import static java.util.prefs.Preferences.userRoot;
39
40
/**
41
 * Persistent options user can change at runtime.
42
 *
43
 * @author Karl Tauber and White Magic Software, Ltd.
44
 */
45
public class DefaultOptions implements Options {
46
47
  private Preferences mPreferences;
48
49
  public DefaultOptions() {
50
    setPreferences( getRootPreferences().node( PREFS_OPTIONS ) );
51
  }
52
53
  /**
54
   * This will throw IllegalArgumentException if the value exceeds the maximum
55
   * preferences value length.
56
   *
57
   * @param key   The name of the key to associate with the value.
58
   * @param value The value to persist.
59
   * @throws BackingStoreException New value not persisted.
60
   */
61
  @Override
62
  public void put( final String key, final String value )
63
      throws BackingStoreException {
64
    getState().put( key, value );
65
    getState().flush();
66
  }
67
68
  @Override
69
  public String get( final String key, final String value ) {
70
    return getState().get( key, value );
71
  }
72
73
  @Override
74
  public String get( final String key ) {
75
    return get( key, "" );
76
  }
77
78
  private void setPreferences( final Preferences preferences ) {
79
    mPreferences = preferences;
80
  }
81
82
  private Preferences getRootPreferences() {
83
    return userRoot().node( PREFS_ROOT );
84
  }
85
86
  @Override
87
  public Preferences getState() {
88
    return getRootPreferences().node( PREFS_STATE );
89
  }
90
}
191
A src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.impl;
29
30
import 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.URISyntaxException;
40
import java.net.URL;
41
import java.nio.charset.Charset;
42
import java.util.Iterator;
43
import java.util.List;
44
45
import static com.scrivenvar.Constants.SETTINGS_NAME;
46
47
/**
48
 * Responsible for loading settings that help avoid hard-coded assumptions.
49
 *
50
 * @author White Magic Software, Ltd.
51
 */
52
public class DefaultSettings implements Settings {
53
54
  private static final char VALUE_SEPARATOR = ',';
55
56
  private PropertiesConfiguration properties;
57
58
  public DefaultSettings()
59
      throws ConfigurationException, URISyntaxException, IOException {
60
    setProperties( createProperties() );
61
  }
62
63
  /**
64
   * Returns the value of a string property.
65
   *
66
   * @param property     The property key.
67
   * @param defaultValue The value to return if no property key has been set.
68
   * @return The property key value, or defaultValue when no key found.
69
   */
70
  @Override
71
  public String getSetting( final String property, final String defaultValue ) {
72
    return getSettings().getString( property, defaultValue );
73
  }
74
75
  /**
76
   * Returns the value of a string property.
77
   *
78
   * @param property     The property key.
79
   * @param defaultValue The value to return if no property key has been set.
80
   * @return The property key value, or defaultValue when no key found.
81
   */
82
  @Override
83
  public int getSetting( final String property, final int defaultValue ) {
84
    return getSettings().getInt( property, defaultValue );
85
  }
86
87
  /**
88
   * Changes key's value. This will clear the old value before setting the new
89
   * value so that the old value is erased, not changed into a list.
90
   *
91
   * @param key   The property key name to obtain its value.
92
   * @param value The new value to set.
93
   */
94
  @Override
95
  public void putSetting( final String key, final String value ) {
96
    getSettings().clearProperty( key );
97
    getSettings().addProperty( key, value );
98
  }
99
100
  /**
101
   * Convert the generic list of property objects into strings.
102
   *
103
   * @param property The property value to coerce.
104
   * @param defaults The defaults values to use should the property be unset.
105
   * @return The list of properties coerced from objects to strings.
106
   */
107
  @Override
108
  public List<String> getStringSettingList(
109
      final String property, final List<String> defaults ) {
110
    return getSettings().getList( String.class, property, defaults );
111
  }
112
113
  /**
114
   * Convert a list of property objects into strings, with no default value.
115
   *
116
   * @param property The property value to coerce.
117
   * @return The list of properties coerced from objects to strings.
118
   */
119
  @Override
120
  public List<String> getStringSettingList( final String property ) {
121
    return getStringSettingList( property, null );
122
  }
123
124
  /**
125
   * Returns a list of property names that begin with the given prefix.
126
   *
127
   * @param prefix The prefix to compare against each property name.
128
   * @return The list of property names that have the given prefix.
129
   */
130
  @Override
131
  public Iterator<String> getKeys( final String prefix ) {
132
    return getSettings().getKeys( prefix );
133
  }
134
135
  private PropertiesConfiguration createProperties()
136
      throws ConfigurationException {
137
138
    final URL url = getPropertySource();
139
    final PropertiesConfiguration configuration = new PropertiesConfiguration();
140
141
    if( url != null ) {
142
      try( final Reader r = new InputStreamReader( url.openStream(),
143
                                                   getDefaultEncoding() ) ) {
144
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
145
        configuration.read( r );
146
147
      } catch( final IOException ex ) {
148
        throw new RuntimeException( new ConfigurationException( ex ) );
149
      }
150
    }
151
152
    return configuration;
153
  }
154
155
  protected Charset getDefaultEncoding() {
156
    return Charset.defaultCharset();
157
  }
158
159
  protected ListDelimiterHandler createListDelimiterHandler() {
160
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
161
  }
162
163
  private URL getPropertySource() {
164
    return DefaultSettings.class.getResource( getSettingsFilename() );
165
  }
166
167
  private String getSettingsFilename() {
168
    return SETTINGS_NAME;
169
  }
170
171
  private void setProperties( final PropertiesConfiguration configuration ) {
172
    this.properties = configuration;
173
  }
174
175
  private PropertiesConfiguration getSettings() {
176
    return this.properties;
177
  }
178
}
1179
A src/main/java/com/scrivenvar/service/impl/DefaultSnitch.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.impl;
29
30
import 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
 * @author White Magic Software, Ltd.
48
 */
49
public class DefaultSnitch extends Observable implements Snitch {
50
51
  /**
52
   * Service for listening to directories for modifications.
53
   */
54
  private WatchService watchService;
55
56
  /**
57
   * Directories being monitored for changes.
58
   */
59
  private Map<WatchKey, Path> keys;
60
61
  /**
62
   * Files that will kick off notification events if modified.
63
   */
64
  private Set<Path> eavesdropped;
65
66
  /**
67
   * Set to true when running; set to false to stop listening.
68
   */
69
  private volatile boolean listening;
70
71
  public DefaultSnitch() {
72
  }
73
74
  @Override
75
  public void stop() {
76
    setListening( false );
77
  }
78
79
  /**
80
   * Adds a listener to the list of files to watch for changes. If the file is
81
   * already in the monitored list, this will return immediately.
82
   *
83
   * @param file Path to a file to watch for changes.
84
   * @throws IOException The file could not be monitored.
85
   */
86
  @Override
87
  public void listen( final Path file ) throws IOException {
88
    if( file != null && getEavesdropped().add( file ) ) {
89
      final Path dir = toDirectory( file );
90
      final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY );
91
92
      getWatchMap().put( key, dir );
93
    }
94
  }
95
96
  /**
97
   * Returns the given path to a file (or directory) as a directory. If the
98
   * given path is already a directory, it is returned. Otherwise, this returns
99
   * the directory that contains the file. This will fail if the file is stored
100
   * in the root folder.
101
   *
102
   * @param path The file to return as a directory, which should always be the
103
   *             case.
104
   * @return The given path as a directory, if a file, otherwise the path
105
   * itself.
106
   */
107
  private Path toDirectory( final Path path ) {
108
    return Files.isDirectory( path )
109
        ? path
110
        : path.toFile().getParentFile().toPath();
111
  }
112
113
  /**
114
   * Stop listening to the given file for change events. This fails silently.
115
   *
116
   * @param file The file to no longer monitor for changes.
117
   */
118
  @Override
119
  public void ignore( final Path file ) {
120
    if( file != null ) {
121
      final Path directory = toDirectory( file );
122
123
      // Remove all occurrences (there should be only one).
124
      getWatchMap().values().removeAll( Collections.singleton( directory ) );
125
126
      // Remove all occurrences (there can be only one).
127
      getEavesdropped().remove( file );
128
    }
129
  }
130
131
  /**
132
   * Loops until stop is called, or the application is terminated.
133
   */
134
  @Override
135
  @SuppressWarnings("BusyWait")
136
  public void run() {
137
    setListening( true );
138
139
    while( isListening() ) {
140
      try {
141
        final WatchKey key = getWatchService().take();
142
        final Path path = get( key );
143
144
        // Prevent receiving two separate ENTRY_MODIFY events: file modified
145
        // and timestamp updated. Instead, receive one ENTRY_MODIFY event
146
        // with two counts.
147
        Thread.sleep( APP_WATCHDOG_TIMEOUT );
148
149
        for( final WatchEvent<?> event : key.pollEvents() ) {
150
          final Path changed = path.resolve( (Path) event.context() );
151
152
          if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
153
            setChanged();
154
            notifyObservers( changed );
155
          }
156
        }
157
158
        if( !key.reset() ) {
159
          ignore( path );
160
        }
161
      } catch( final IOException | InterruptedException ex ) {
162
        // Stop eavesdropping.
163
        setListening( false );
164
      }
165
    }
166
  }
167
168
  /**
169
   * Returns true if the list of files being listened to for changes contains
170
   * the given file.
171
   *
172
   * @param file Path to a system file.
173
   * @return true The given file is being monitored for changes.
174
   */
175
  private boolean isListening( final Path file ) {
176
    return getEavesdropped().contains( file );
177
  }
178
179
  /**
180
   * Returns a path for a given watch key.
181
   *
182
   * @param key The key to lookup its corresponding path.
183
   * @return The path for the given key.
184
   */
185
  private Path get( final WatchKey key ) {
186
    return getWatchMap().get( key );
187
  }
188
189
  private synchronized Map<WatchKey, Path> getWatchMap() {
190
    if( this.keys == null ) {
191
      this.keys = createWatchKeys();
192
    }
193
194
    return this.keys;
195
  }
196
197
  protected Map<WatchKey, Path> createWatchKeys() {
198
    return new ConcurrentHashMap<>();
199
  }
200
201
  /**
202
   * Returns a list of files that, when changed, will kick off a notification.
203
   *
204
   * @return A non-null, possibly empty, list of files.
205
   */
206
  private synchronized Set<Path> getEavesdropped() {
207
    if( this.eavesdropped == null ) {
208
      this.eavesdropped = createEavesdropped();
209
    }
210
211
    return this.eavesdropped;
212
  }
213
214
  protected Set<Path> createEavesdropped() {
215
    return ConcurrentHashMap.newKeySet();
216
  }
217
218
  /**
219
   * The existing watch service, or a new instance if null.
220
   *
221
   * @return A valid WatchService instance, never null.
222
   * @throws IOException Could not create a new watch service.
223
   */
224
  private synchronized WatchService getWatchService() throws IOException {
225
    if( this.watchService == null ) {
226
      this.watchService = createWatchService();
227
    }
228
229
    return this.watchService;
230
  }
231
232
  protected WatchService createWatchService() throws IOException {
233
    final FileSystem fileSystem = FileSystems.getDefault();
234
    return fileSystem.newWatchService();
235
  }
236
237
  /**
238
   * Answers whether the loop should continue executing.
239
   *
240
   * @return true The internal listening loop should continue listening for file
241
   * modification events.
242
   */
243
  protected boolean isListening() {
244
    return this.listening;
245
  }
246
247
  /**
248
   * Requests the snitch to stop eavesdropping on file changes.
249
   *
250
   * @param listening Use true to indicate the service should stop running.
251
   */
252
  private void setListening( final boolean listening ) {
253
    this.listening = listening;
254
  }
255
}
1256
A src/main/java/com/scrivenvar/util/Action.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import de.jensd.fx.glyphs.GlyphIcons;
30
import javafx.beans.value.ObservableBooleanValue;
31
import javafx.event.ActionEvent;
32
import javafx.event.EventHandler;
33
import javafx.scene.input.KeyCombination;
34
35
/**
36
 * Simple action class
37
 *
38
 * @author Karl Tauber
39
 */
40
public class Action {
41
42
  public final String text;
43
  public final KeyCombination accelerator;
44
  public final GlyphIcons icon;
45
  public final EventHandler<ActionEvent> action;
46
  public final ObservableBooleanValue disable;
47
48
  public Action(
49
    final String text,
50
    final String accelerator,
51
    final GlyphIcons icon,
52
    final EventHandler<ActionEvent> action ) {
53
    this( text, accelerator, icon, action, null );
54
  }
55
56
  public Action(
57
    final String text,
58
    final String accelerator,
59
    final GlyphIcons icon,
60
    final EventHandler<ActionEvent> action,
61
    final ObservableBooleanValue disable ) {
62
63
    this.text = text;
64
    this.accelerator = (accelerator != null)
65
      ? KeyCombination.valueOf( accelerator )
66
      : null;
67
    this.icon = icon;
68
    this.action = action;
69
    this.disable = disable;
70
  }
71
}
172
A src/main/java/com/scrivenvar/util/ActionUtils.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
30
import javafx.scene.Node;
31
import javafx.scene.control.Button;
32
import javafx.scene.control.Menu;
33
import javafx.scene.control.MenuItem;
34
import javafx.scene.control.Separator;
35
import javafx.scene.control.SeparatorMenuItem;
36
import javafx.scene.control.ToolBar;
37
import javafx.scene.control.Tooltip;
38
39
/**
40
 * Action utilities
41
 *
42
 * @author Karl Tauber
43
 */
44
public class ActionUtils {
45
46
  public static Menu createMenu( final String text, final Action... actions ) {
47
    return new Menu( text, null, createMenuItems( actions ) );
48
  }
49
50
  public static MenuItem[] createMenuItems( Action... actions ) {
51
    MenuItem[] menuItems = new MenuItem[ actions.length ];
52
    for( int i = 0; i < actions.length; i++ ) {
53
      menuItems[ i ] = (actions[ i ] != null)
54
        ? createMenuItem( actions[ i ] )
55
        : new SeparatorMenuItem();
56
    }
57
    return menuItems;
58
  }
59
60
  public static MenuItem createMenuItem( Action action ) {
61
    MenuItem menuItem = new MenuItem( action.text );
62
    if( action.accelerator != null ) {
63
      menuItem.setAccelerator( action.accelerator );
64
    }
65
66
    if( action.icon != null ) {
67
      menuItem.setGraphic( FontAwesomeIconFactory.get().createIcon( action.icon ) );
68
    }
69
70
    menuItem.setOnAction( action.action );
71
72
    if( action.disable != null ) {
73
      menuItem.disableProperty().bind( action.disable );
74
    }
75
76
    menuItem.setMnemonicParsing( true );
77
78
    return menuItem;
79
  }
80
81
  public static ToolBar createToolBar( Action... actions ) {
82
    return new ToolBar( createToolBarButtons( actions ) );
83
  }
84
85
  public static Node[] createToolBarButtons( Action... actions ) {
86
    Node[] buttons = new Node[ actions.length ];
87
    for( int i = 0; i < actions.length; i++ ) {
88
      buttons[ i ] = (actions[ i ] != null)
89
        ? createToolBarButton( actions[ i ] )
90
        : new Separator();
91
    }
92
    return buttons;
93
  }
94
95
  public static Button createToolBarButton( Action action ) {
96
    Button button = new Button();
97
    button.setGraphic( FontAwesomeIconFactory.get().createIcon( action.icon, "1.2em" ) );
98
    String tooltip = action.text;
99
    if( tooltip.endsWith( "..." ) ) {
100
      tooltip = tooltip.substring( 0, tooltip.length() - 3 );
101
    }
102
    if( action.accelerator != null ) {
103
      tooltip += " (" + action.accelerator.getDisplayText() + ')';
104
    }
105
    button.setTooltip( new Tooltip( tooltip ) );
106
    button.setFocusTraversable( false );
107
    button.setOnAction( action.action );
108
    if( action.disable != null ) {
109
      button.disableProperty().bind( action.disable );
110
    }
111
    return button;
112
  }
113
}
1114
A src/main/java/com/scrivenvar/util/Lists.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.util;
29
30
import java.util.List;
31
32
/**
33
 * Convenience class that provides a clearer API for obtaining list elements.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public final class Lists {
38
39
  private Lists() {
40
  }
41
42
  /**
43
   * Returns the first item in the given list, or null if not found.
44
   *
45
   * @param <T> The generic list type.
46
   * @param list The list that may have a first item.
47
   *
48
   * @return null if the list is null or there is no first item.
49
   */
50
  public static <T> T getFirst( final List<T> list ) {
51
    return getFirst( list, null );
52
  }
53
54
  /**
55
   * Returns the last item in the given list, or null if not found.
56
   *
57
   * @param <T> The generic list type.
58
   * @param list The list that may have a last item.
59
   *
60
   * @return null if the list is null or there is no last item.
61
   */
62
  public static <T> T getLast( final List<T> list ) {
63
    return getLast( list, null );
64
  }
65
66
  /**
67
   * Returns the first item in the given list, or t if not found.
68
   *
69
   * @param <T> The generic list type.
70
   * @param list The list that may have a first item.
71
   * @param t The default return value.
72
   *
73
   * @return null if the list is null or there is no first item.
74
   */
75
  public static <T> T getFirst( final List<T> list, final T t ) {
76
    return isEmpty( list ) ? t : list.get( 0 );
77
  }
78
79
  /**
80
   * Returns the last item in the given list, or t if not found.
81
   *
82
   * @param <T> The generic list type.
83
   * @param list The list that may have a last item.
84
   * @param t The default return value.
85
   *
86
   * @return null if the list is null or there is no last item.
87
   */
88
  public static <T> T getLast( final List<T> list, final T t ) {
89
    return isEmpty( list ) ? t : list.get( list.size() - 1 );
90
  }
91
92
  /**
93
   * Returns true if the given list is null or empty.
94
   *
95
   * @param <T> The generic list type.
96
   * @param list The list that has a last item.
97
   *
98
   * @return true The list is empty.
99
   */
100
  public static <T> boolean isEmpty( final List<T> list ) {
101
    return list == null || list.isEmpty();
102
  }
103
}
1104
A src/main/java/com/scrivenvar/util/StageState.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import java.util.prefs.Preferences;
30
import javafx.application.Platform;
31
import javafx.scene.shape.Rectangle;
32
import javafx.stage.Stage;
33
import javafx.stage.WindowEvent;
34
35
/**
36
 * Saves and restores Stage state (window bounds, maximized, fullScreen).
37
 *
38
 * @author Karl Tauber
39
 */
40
public class StageState {
41
42
  public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition";
43
  public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor";
44
  public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview";
45
46
  private final Stage stage;
47
  private final Preferences state;
48
49
  private Rectangle normalBounds;
50
  private boolean runLaterPending;
51
52
  public StageState( final Stage stage, final Preferences state ) {
53
    this.stage = stage;
54
    this.state = state;
55
56
    restore();
57
58
    stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() );
59
60
    stage.xProperty().addListener( (ob, o, n) -> boundsChanged() );
61
    stage.yProperty().addListener( (ob, o, n) -> boundsChanged() );
62
    stage.widthProperty().addListener( (ob, o, n) -> boundsChanged() );
63
    stage.heightProperty().addListener( (ob, o, n) -> boundsChanged() );
64
  }
65
66
  private void save() {
67
    final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds;
68
    
69
    if( bounds != null ) {
70
      state.putDouble( "windowX", bounds.getX() );
71
      state.putDouble( "windowY", bounds.getY() );
72
      state.putDouble( "windowWidth", bounds.getWidth() );
73
      state.putDouble( "windowHeight", bounds.getHeight() );
74
    }
75
    
76
    state.putBoolean( "windowMaximized", stage.isMaximized() );
77
    state.putBoolean( "windowFullScreen", stage.isFullScreen() );
78
  }
79
80
  private void restore() {
81
    final double x = state.getDouble( "windowX", Double.NaN );
82
    final double y = state.getDouble( "windowY", Double.NaN );
83
    final double w = state.getDouble( "windowWidth", Double.NaN );
84
    final double h = state.getDouble( "windowHeight", Double.NaN );
85
    final boolean maximized = state.getBoolean( "windowMaximized", false );
86
    final boolean fullScreen = state.getBoolean( "windowFullScreen", false );
87
88
    if( !Double.isNaN( x ) && !Double.isNaN( y ) ) {
89
      stage.setX( x );
90
      stage.setY( y );
91
    } // else: default behavior is center on screen
92
93
    if( !Double.isNaN( w ) && !Double.isNaN( h ) ) {
94
      stage.setWidth( w );
95
      stage.setHeight( h );
96
    } // else: default behavior is use scene size
97
98
    if( fullScreen != stage.isFullScreen() ) {
99
      stage.setFullScreen( fullScreen );
100
    }
101
    
102
    if( maximized != stage.isMaximized() ) {
103
      stage.setMaximized( maximized );
104
    }
105
  }
106
107
  /**
108
   * Remembers the window bounds when the window is not iconified, maximized or
109
   * in fullScreen.
110
   */
111
  private void boundsChanged() {
112
    // avoid too many (and useless) runLater() invocations
113
    if( runLaterPending ) {
114
      return;
115
    }
116
    
117
    runLaterPending = true;
118
119
    // must use runLater() to ensure that change of all properties
120
    // (x, y, width, height, iconified, maximized and fullScreen)
121
    // has finished
122
    Platform.runLater( () -> {
123
      runLaterPending = false;
124
125
      if( isNormalState() ) {
126
        normalBounds = getStageBounds();
127
      }
128
    } );
129
  }
130
131
  private boolean isNormalState() {
132
    return !stage.isIconified() && !stage.isMaximized() && !stage.isFullScreen();
133
  }
134
135
  private Rectangle getStageBounds() {
136
    return new Rectangle( stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight() );
137
  }
138
}
1139
A src/main/java/com/scrivenvar/util/Utils.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import java.util.ArrayList;
30
import java.util.prefs.Preferences;
31
32
/**
33
 * @author Karl Tauber and White Magic Software, Ltd.
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/build.sh
1
#!/bin/bash
2
3
INKSCAPE="/usr/bin/inkscape"
4
PNG_COMPRESS="optipng"
5
PNG_COMPRESS_OPTS="-o9 *png"
6
ICO_TOOL="icotool"
7
ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png"
8
9
declare -a SIZES=("16" "32" "64" "128" "256" "512")
10
11
for i in "${SIZES[@]}"; do
12
  # -y: export background opacity 0
13
  $INKSCAPE -y 0 -z -f "logo.svg" -w "${i}" -e "logo${i}.png"
14
done
15
16
# Compess the PNG images.
17
which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS
18
19
# Generate an ICO file.
20
which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS
21
122
A src/main/resources/com/scrivenvar/editor/markdown.css
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
.markdown-editor {
29
  -fx-font-size: 14px;
30
}
31
32
/*---- headers ----*/
33
34
.markdown-editor .h1 { -fx-font-size: 2.25em; }
35
.markdown-editor .h2 { -fx-font-size: 1.75em; }
36
.markdown-editor .h3 { -fx-font-size: 1.5em; }
37
.markdown-editor .h4 { -fx-font-size: 1.25em; }
38
.markdown-editor .h5 { -fx-font-size: 1.1em; }
39
.markdown-editor .h6 { -fx-font-size: 1em; }
40
41
.markdown-editor .h1,
42
.markdown-editor .h2,
43
.markdown-editor .h3,
44
.markdown-editor .h4,
45
.markdown-editor .h5,
46
.markdown-editor .h6 {
47
  -fx-font-weight: bold;
48
  -fx-fill: derive(crimson, -20%);
49
}
50
51
52
/*---- inlines ----*/
53
54
.markdown-editor .strong {
55
  -fx-font-weight: bold;
56
}
57
58
.markdown-editor .em {
59
  -fx-font-style: italic;
60
}
61
62
.markdown-editor .del {
63
  -fx-strikethrough: true;
64
}
65
66
.markdown-editor .a {
67
  -fx-fill: #4183C4 !important;
68
}
69
70
.markdown-editor .img {
71
  -fx-fill: #4183C4 !important;
72
}
73
74
.markdown-editor .code {
75
  -fx-font-family: monospace;
76
  -fx-fill: #090 !important;
77
}
78
79
80
/*---- blocks ----*/
81
82
.markdown-editor .pre {
83
  -fx-font-family: monospace;
84
  -fx-fill: #060 !important;
85
}
86
87
.markdown-editor .blockquote {
88
  -fx-fill: #777;
89
}
90
91
92
/*---- lists ----*/
93
94
.markdown-editor .ul {
95
}
96
97
.markdown-editor .ol {
98
}
99
100
.markdown-editor .li {
101
  -fx-fill: #444;
102
}
103
104
.markdown-editor .dl {
105
}
106
107
.markdown-editor .dt {
108
  -fx-font-weight: bold;
109
  -fx-font-style: italic;
110
}
111
112
.markdown-editor .dd {
113
  -fx-fill: #444;
114
}
115
116
117
/*---- table ----*/
118
119
.markdown-editor .table {
120
  -fx-font-family: monospace;
121
}
122
123
.markdown-editor .thead {
124
}
125
126
.markdown-editor .tbody {
127
}
128
129
.markdown-editor .caption {
130
}
131
132
.markdown-editor .th {
133
  -fx-font-weight: bold;
134
}
135
136
.markdown-editor .tr {
137
}
138
139
.markdown-editor .td {
140
}
141
142
143
/*---- misc ----*/
144
145
.markdown-editor .html {
146
  -fx-font-family: monospace;
147
  -fx-fill: derive(crimson, -50%);
148
}
149
.markdown-editor .monospace {
150
  -fx-font-family: monospace;
151
}
1152
A src/main/resources/com/scrivenvar/logo.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4
<svg
5
   xmlns:dc="http://purl.org/dc/elements/1.1/"
6
   xmlns:cc="http://creativecommons.org/ns#"
7
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8
   xmlns:svg="http://www.w3.org/2000/svg"
9
   xmlns="http://www.w3.org/2000/svg"
10
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
   id="svg2"
13
   version="1.1"
14
   inkscape:version="0.91 r13725"
15
   width="512"
16
   height="512"
17
   viewBox="0 0 512 512"
18
   sodipodi:docname="logo.svg">
19
  <metadata
20
     id="metadata8">
21
    <rdf:RDF>
22
      <cc:Work
23
         rdf:about="">
24
        <dc:format>image/svg+xml</dc:format>
25
        <dc:type
26
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
27
        <dc:title></dc:title>
28
      </cc:Work>
29
    </rdf:RDF>
30
  </metadata>
31
  <defs
32
     id="defs6" />
33
  <sodipodi:namedview
34
     pagecolor="#ffffff"
35
     bordercolor="#666666"
36
     borderopacity="1"
37
     objecttolerance="10"
38
     gridtolerance="10"
39
     guidetolerance="10"
40
     inkscape:pageopacity="0"
41
     inkscape:pageshadow="2"
42
     inkscape:window-width="640"
43
     inkscape:window-height="480"
44
     id="namedview4"
45
     showgrid="false"
46
     fit-margin-top="0"
47
     fit-margin-left="0"
48
     fit-margin-right="0"
49
     fit-margin-bottom="0"
50
     inkscape:zoom="1.2682274"
51
     inkscape:cx="15.646213"
52
     inkscape:cy="213.34955"
53
     inkscape:current-layer="svg2" />
54
  <path
55
     style="fill:#ce6200;fill-opacity:1"
56
     d="m 203.2244,511.85078 c -60.01827,-1.2968 -121.688643,-6.5314 -192.436493,-16.334 -5.8078027,-0.8047 -10.66110747,-1.561 -10.78511762,-1.6806 -0.12404567,-0.1196 3.90488112,-4.5812 8.95313512,-9.9147 32.9484785,-34.8102 70.4314485,-73.8923 104.1521555,-108.5956 l 11.87611,-12.2221 5.48905,-10.2177 c 35.82801,-66.6927 75.13064,-128.5665 105.90637,-166.7277 6.13805,-7.611 10.21451,-12.0689 17.28719,-18.9048 36.6818,-35.4537 108.27279,-83.724003 206.0323,-138.917303 22.10365,-12.47935 51.93386,-28.64995037 52.26391,-28.33165037 0.38883,0.37499 -2.35932,25.95575037 -4.86585,45.29275037 -7.28943,56.236403 -17.04619,103.128903 -28.07642,134.939803 -7.19617,20.7536 -14.81287,35.152 -22.9667,43.4155 -3.60444,3.6529 -6.58328,5.7941 -10.1313,7.2825 l -2.56414,1.0756 -53.43164,0.1713 -53.43166,0.1713 3.69973,1.8547 c 26.78565,13.4282 52.58051,27.5241 59.57122,32.5533 4.48397,3.2259 4.41278,2.9854 1.59124,5.3784 -26.99514,22.8955 -74.52961,44.0013 -140.23089,62.2641 -26.34995,7.3244 -57.85469,14.6842 -86.99871,20.3237 l -10.26943,1.9871 -52.01052,53.2733 -52.010524,53.2732 -29.459801,15.1165 c -26.4100885,13.5517 -29.3446639,15.1388 -28.347645,15.3311 0.6117029,0.118 4.0894221,0.2188 7.7282726,0.2239 3.6388854,0.01 16.1273694,0.2329 27.7522124,0.5059 51.576376,1.2116 146.083985,1.512 170.154295,0.5409 34.66996,-1.3988 52.7606,-2.9325 67.58258,-5.7293 2.68664,-0.507 4.82907,-0.9755 4.76094,-1.0412 -0.0681,-0.066 -3.24733,-0.8833 -7.0649,-1.8169 -8.04133,-1.9664 -25.10167,-5.3107 -41.1231,-8.0612 -47.6405,-8.1787 -65.48708,-12.0107 -74.13028,-15.9169 -3.90548,-1.7651 -7.13816,-4.7659 -8.12937,-7.5463 -1.01822,-2.8562 -0.92214,-6.5271 0.23315,-8.9083 1.86563,-3.8451 6.14837,-6.7199 12.26745,-8.2345 16.96993,-4.2004 57.27977,-6.1832 90.36228,-4.4448 54.7332,2.8761 117.0767,13.1228 178.50212,29.3385 18.03514,4.7611 51.66065,14.656 51.22677,15.0744 -0.0824,0.08 -5.72762,-0.854 -12.54488,-2.0745 -40.1043,-7.18 -60.50854,-10.2888 -101.40822,-15.4507 -24.4851,-3.0902 -55.12614,-5.9915 -77.58876,-7.3465 -26.58826,-1.6039 -61.15821,-1.7754 -80.99202,-0.4019 l -3.19705,0.2214 8.70308,1.4934 c 51.89698,8.9047 77.51746,14.9877 88.00479,20.8948 6.9134,3.894 10.30497,9.4381 9.33333,15.2569 -1.50397,9.0066 -10.51381,14.0257 -32.00273,17.8278 -16.31374,2.8863 -47.27575,4.3845 -77.23553,3.7371 z"
57
     id="path4138" />
58
  <path
59
     style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"
60
     d="m 214.76931,324.51908 c 60.83777,-14.1145 111.89562,-31.6251 144.40025,-49.5229 3.12602,-1.7213 5.81747,-3.2537 5.98106,-3.4054 0.40534,-0.3759 -13.76388,-7.9415 -34.63489,-18.4929 -7.52161,-3.8026 -9.82337,-5.3787 -12.0735,-8.2668 -5.14485,-6.6036 -5.96081,-14.8404 -2.20331,-22.2417 1.80288,-3.5512 5.69484,-7.3007 9.36158,-9.019 5.20851,-2.4407 1.18148,-2.2865 59.71223,-2.2865 l 52.81361,0 2.13233,-2.1984 c 2.78673,-2.8731 5.23414,-6.4981 8.23035,-12.1905 14.14966,-26.8827 26.71842,-78.3816 36.24347,-148.503303 0.76704,-5.6468 1.36194,-10.2983 1.32201,-10.3369 -0.0399,-0.038 -5.47754,2.9629 -12.08361,6.6697 l -12.01104,6.7396 -133.83068,137.037303 c -73.60688,75.3705 -134.81732,138.0567 -136.0232,139.3026 l -2.19251,2.2653 8.254,-1.8067 c 4.53969,-0.9937 12.01053,-2.6783 16.60185,-3.7435 z"
61
     id="path4136" />
62
  <path
63
     style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"
64
     d="m 202.72524,284.43588 c 69.93294,-70.1332 135.4799,-131.9279 213.46406,-201.244203 7.71421,-6.8568 14.50542,-12.9341 15.09155,-13.5052 0.9482,-0.9239 0.96778,-0.9811 0.17761,-0.5188 -77.96496,45.611803 -139.23519,88.710503 -166.72539,117.278203 -18.81811,19.5556 -50.35654,64.861 -80.96704,116.3104 -0.91787,1.5427 1.02249,-0.3323 18.95921,-18.3204 z"
65
     id="path4142" />
66
  <path
67
     style="fill:#000000"
68
     d=""
69
     id="path4140"
70
     inkscape:connector-curvature="0" />
71
</svg>
172
A src/main/resources/com/scrivenvar/logo128.png
Binary file
A src/main/resources/com/scrivenvar/logo16.png
Binary file
A src/main/resources/com/scrivenvar/logo256.png
Binary file
A src/main/resources/com/scrivenvar/logo32.png
Binary file
A src/main/resources/com/scrivenvar/logo512.png
Binary file
A src/main/resources/com/scrivenvar/logo64.png
Binary file
A src/main/resources/com/scrivenvar/messages.properties
1
#
2
# Copyright 2017 Karl Tauber and White Magic Software, Ltd.
3
#
4
# All rights reserved.
5
#
6
# Redistribution and use in source and binary forms, with or without
7
# modification, are permitted provided that the following conditions are met:
8
#
9
#  * Redistributions of source code must retain the above copyright
10
#    notice, this list of conditions and the following disclaimer.
11
#
12
#  * Redistributions in binary form must reproduce the above copyright
13
#    notice, this list of conditions and the following disclaimer in the
14
#    documentation and/or other materials provided with the distribution.
15
#
16
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
#
28
29
# ########################################################################
30
#
31
# Main Application Window
32
#
33
# ########################################################################
34
35
# The application title should exist only once in the entire code base.
36
# All other references should either refer to this value via the Messages
37
# class, or indirectly using ${Main.title}.
38
Main.title=Scrivenvar
39
40
Main.menu.file=_File
41
Main.menu.file.new=_New
42
Main.menu.file.open=_Open...
43
Main.menu.file.close=_Close
44
Main.menu.file.close_all=Close All
45
Main.menu.file.save=_Save
46
Main.menu.file.save_as=Save _As
47
Main.menu.file.save_all=Save A_ll
48
Main.menu.file.exit=E_xit
49
50
Main.menu.edit=_Edit
51
Main.menu.edit.undo=_Undo
52
Main.menu.edit.redo=_Redo
53
Main.menu.edit.find=_Find
54
Main.menu.edit.find.replace=Re_place
55
Main.menu.edit.find.next=Find _Next
56
Main.menu.edit.find.previous=Find _Previous
57
58
Main.menu.insert=_Insert
59
Main.menu.insert.bold=Bold
60
Main.menu.insert.italic=Italic
61
Main.menu.insert.superscript=Superscript
62
Main.menu.insert.subscript=Subscript
63
Main.menu.insert.strikethrough=Strikethrough
64
Main.menu.insert.blockquote=Blockquote
65
Main.menu.insert.code=Inline Code
66
Main.menu.insert.fenced_code_block=Fenced Code Block
67
Main.menu.insert.fenced_code_block.prompt=Enter code here
68
Main.menu.insert.link=Link...
69
Main.menu.insert.image=Image...
70
Main.menu.insert.header_1=Header 1
71
Main.menu.insert.header_1.prompt=header 1
72
Main.menu.insert.header_2=Header 2
73
Main.menu.insert.header_2.prompt=header 2
74
Main.menu.insert.header_3=Header 3
75
Main.menu.insert.header_3.prompt=header 3
76
Main.menu.insert.header_4=Header 4
77
Main.menu.insert.header_4.prompt=header 4
78
Main.menu.insert.header_5=Header 5
79
Main.menu.insert.header_5.prompt=header 5
80
Main.menu.insert.header_6=Header 6
81
Main.menu.insert.header_6.prompt=header 6
82
Main.menu.insert.unordered_list=Unordered List
83
Main.menu.insert.ordered_list=Ordered List
84
Main.menu.insert.horizontal_rule=Horizontal Rule
85
Main.menu.r=_R
86
Main.menu.r.script=_Script
87
Main.menu.r.directory=_Directory
88
89
Main.menu.help=_Help
90
Main.menu.help.about=About ${Main.title}
91
92
# ########################################################################
93
#
94
# Status Bar
95
#
96
# ########################################################################
97
98
Main.statusbar.text.offset=offset
99
Main.statusbar.line=Line {0} of {1}, ${Main.statusbar.text.offset} {2}
100
Main.statusbar.state.default=OK
101
Main.statusbar.parse.error={0} (near ${Main.statusbar.text.offset} {1})
102
103
# ########################################################################
104
#
105
# Definition Pane and its Tree View
106
#
107
# ########################################################################
108
109
Definition.menu.add=Add
110
Definition.menu.remove=Delete
111
112
# ########################################################################
113
#
114
# File Editor
115
#
116
# ########################################################################
117
118
FileEditor.untitled=Untitled
119
FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1}
120
FileEditor.loadFailed.title=Load
121
FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
122
FileEditor.saveFailed.title=Save
123
124
# ########################################################################
125
#
126
# File Open
127
#
128
# ########################################################################
129
130
Dialog.file.choose.open.title=Open File
131
Dialog.file.choose.save.title=Save File
132
133
Dialog.file.choose.filter.title.source=Source Files
134
Dialog.file.choose.filter.title.definition=Definition Files
135
Dialog.file.choose.filter.title.xml=XML Files
136
Dialog.file.choose.filter.title.all=All Files
137
138
# ########################################################################
139
#
140
# Alert Dialog
141
#
142
# ########################################################################
143
144
Alert.file.close.title=Close
145
Alert.file.close.text=Save changes to {0}?
146
147
# ########################################################################
148
#
149
# Definition Pane
150
#
151
# ########################################################################
152
153
Pane.defintion.node.root.title=Definitions
154
155
# Controls ###############################################################
156
157
# ########################################################################
158
#
159
# Browse Directory
160
#
161
# ########################################################################
162
163
BrowseDirectoryButton.chooser.title=Browse for local folder
164
BrowseDirectoryButton.tooltip=${BrowseDirectoryButton.chooser.title}
165
166
# ########################################################################
167
#
168
# Browse File
169
#
170
# ########################################################################
171
172
BrowseFileButton.chooser.title=Browse for local file
173
BrowseFileButton.chooser.allFilesFilter=All Files
174
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
175
176
# Dialogs ################################################################
177
178
# ########################################################################
179
#
180
# Image
181
#
182
# ########################################################################
183
184
Dialog.image.title=Image
185
Dialog.image.chooser.imagesFilter=Images
186
Dialog.image.previewLabel.text=Markdown Preview\:
187
Dialog.image.textLabel.text=Alternate Text\:
188
Dialog.image.titleLabel.text=Title (tooltip)\:
189
Dialog.image.urlLabel.text=Image URL\:
190
191
# ########################################################################
192
#
193
# Hyperlink
194
#
195
# ########################################################################
196
197
Dialog.link.title=Link
198
Dialog.link.previewLabel.text=Markdown Preview\:
199
Dialog.link.textLabel.text=Link Text\:
200
Dialog.link.titleLabel.text=Title (tooltip)\:
201
Dialog.link.urlLabel.text=Link URL\:
202
203
# ########################################################################
204
#
205
# About
206
#
207
# ########################################################################
208
209
Dialog.about.title=About
210
Dialog.about.header=${Main.title}
211
Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
212
213
# R ################################################################
214
215
# ########################################################################
216
#
217
# R Script
218
#
219
# ########################################################################
220
221
Dialog.r.script.title=R Startup Script
222
Dialog.r.script.content=Provide R statements to run prior to interpreting R statements embedded in the document.
223
224
# ########################################################################
225
#
226
# R Directory
227
#
228
# ########################################################################
229
230
Dialog.r.directory.title=Bootstrap Working Directory
231
Dialog.r.directory.header=Value for $application.r.working.directory$.
232
233
# Options ################################################################
234
235
# ########################################################################
236
#
237
# Options Dialog
238
#
239
# ########################################################################
240
241
OptionsDialog.title=Options
242
OptionsDialog.generalTab.text=General
243
OptionsDialog.markdownTab.text=Markdown
244
245
# ########################################################################
246
#
247
# General Options Pane
248
#
249
# ########################################################################
250
251
GeneralOptionsPane.encodingLabel.text=En_coding\:
252
GeneralOptionsPane.lineSeparatorLabel.text=_Line separator\:
253
GeneralOptionsPane.lineSeparatorLabel2.text=(applies to new files only)
254
255
GeneralOptionsPane.platformDefault=Platform Default ({0})
256
GeneralOptionsPane.sepWindows=Windows (CRLF)
257
GeneralOptionsPane.sepUnix=Unix (LF)
258
259
# ########################################################################
260
#
261
# Markdown Options Pane
262
#
263
# ########################################################################
264
265
MarkdownOptionsPane.abbreviationsExtCheckBox.text=A_bbreviations in the way of
266
MarkdownOptionsPane.abbreviationsExtLink.text=Markdown Extra
267
MarkdownOptionsPane.anchorlinksExtCheckBox.text=_Anchor links in headers
268
MarkdownOptionsPane.atxHeaderSpaceExtCheckBox.text=Requires a space char after Atx \# header prefixes, so that \#dasdsdaf is not a header
269
MarkdownOptionsPane.autolinksExtCheckBox.text=_Plain (undelimited) autolinks in the way of
270
MarkdownOptionsPane.autolinksExtLink.text=Github-flavoured-Markdown
271
MarkdownOptionsPane.definitionListsExtCheckBox.text=_Definition lists in the way of
272
MarkdownOptionsPane.definitionListsExtLink.text=Markdown Extra
273
MarkdownOptionsPane.extAnchorLinksExtCheckBox.text=Generate anchor links for headers using complete contents of the header
274
MarkdownOptionsPane.fencedCodeBlocksExtCheckBox.text=_Fenced Code Blocks in the way of
275
MarkdownOptionsPane.fencedCodeBlocksExtLabel.text=or
276
MarkdownOptionsPane.fencedCodeBlocksExtLink.text=Markdown Extra
277
MarkdownOptionsPane.fencedCodeBlocksExtLink2.text=Github-flavoured-Markdown
278
MarkdownOptionsPane.forceListItemParaExtCheckBox.text=Force List and Definition Paragraph wrapping if it includes more than just a single paragraph
279
MarkdownOptionsPane.hardwrapsExtCheckBox.text=_Newlines in paragraph-like content as real line breaks, see
280
MarkdownOptionsPane.hardwrapsExtLink.text=Github-flavoured-Markdown
281
MarkdownOptionsPane.quotesExtCheckBox.text=Beautify single _quotes, double quotes and double angle quotes (\u00ab and \u00bb)
282
MarkdownOptionsPane.relaxedHrRulesExtCheckBox.text=Allow horizontal rules without a blank line following them
283
MarkdownOptionsPane.smartsExtCheckBox.text=Beautify apostrophes, _ellipses ("..." and ". . .") and dashes ("--" and "---")
284
MarkdownOptionsPane.strikethroughExtCheckBox.text=_Strikethrough
285
MarkdownOptionsPane.suppressHtmlBlocksExtCheckBox.text=Suppress the _output of HTML blocks
286
MarkdownOptionsPane.suppressInlineHtmlExtCheckBox.text=Suppress the o_utput of inline HTML elements
287
MarkdownOptionsPane.tablesExtCheckBox.text=_Tables similar to
288
MarkdownOptionsPane.tablesExtLabel.text=(like
289
MarkdownOptionsPane.tablesExtLabel2.text=tables, but with colspan support)
290
MarkdownOptionsPane.tablesExtLink.text=MultiMarkdown
291
MarkdownOptionsPane.tablesExtLink2.text=Markdown Extra
292
MarkdownOptionsPane.taskListItemsExtCheckBox.text=GitHub style task list items
293
MarkdownOptionsPane.wikilinksExtCheckBox.text=_Wiki-style links ("[[wiki link]]")
1294
A src/main/resources/com/scrivenvar/preview/webview.css
1
/*
2
This software is released under the MIT license:
3
4
Permission is hereby granted, free of charge, to any person obtaining a copy of
5
this software and associated documentation files (the "Software"), to deal in
6
the Software without restriction, including without limitation the rights to
7
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
the Software, and to permit persons to whom the Software is furnished to do so,
9
subject to the following conditions:
10
11
The above copyright notice and this permission notice shall be included in all
12
copies or substantial portions of the Software.
13
14
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
*/
21
22
/* Source: https://github.com/nicolashery/markdownpad-github */
23
24
/*  GitHub stylesheet for MarkdownPad (http://markdownpad.com) */
25
26
/* RESET
27
=============================================================================*/
28
29
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6,
30
p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn,
31
em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var,
32
b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label,
33
legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas,
34
details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output,
35
ruby, section, summary, time, mark, audio, video {
36
  margin: 0;
37
  padding: 0;
38
  border: 0;
39
}
40
41
/* BODY
42
=============================================================================*/
43
44
body {
45
  font-family: Helvetica, arial, freesans, clean, sans-serif;
46
  font-size: 14px;
47
  line-height: 1.6;
48
  color: #333;
49
  background-color: #fff;
50
  padding: 20px;
51
  max-width: 960px;
52
  margin: 0 auto;
53
}
54
55
body>*:first-child {
56
  margin-top: 0 !important;
57
}
58
59
body>*:last-child {
60
  margin-bottom: 0 !important;
61
}
62
63
/* BLOCKS
64
=============================================================================*/
65
66
p, blockquote, ul, ol, dl, table, pre {
67
  margin: 15px 0;
68
}
69
70
/* HEADERS
71
=============================================================================*/
72
73
h1, h2, h3, h4, h5, h6 {
74
  margin: 20px 0 10px;
75
  padding: 0;
76
  font-weight: bold;
77
  -webkit-font-smoothing: antialiased;
78
}
79
80
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
81
h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
82
  font-size: inherit;
83
}
84
85
h1 {
86
  font-size: 28px;
87
  color: #000;
88
}
89
90
h2 {
91
  font-size: 24px;
92
  border-bottom: 1px solid #ccc;
93
  color: #000;
94
}
95
96
h3 {
97
  font-size: 18px;
98
}
99
100
h4 {
101
  font-size: 16px;
102
}
103
104
h5 {
105
  font-size: 14px;
106
}
107
108
h6 {
109
  color: #777;
110
  font-size: 14px;
111
}
112
113
body>h2:first-child, body>h1:first-child, body>h1:first-child+h2,
114
body>h3:first-child, body>h4:first-child, body>h5:first-child,
115
body>h6:first-child {
116
  margin-top: 0;
117
  padding-top: 0;
118
}
119
120
a:first-child h1, a:first-child h2, a:first-child h3,
121
a:first-child h4, a:first-child h5, a:first-child h6 {
122
  margin-top: 0;
123
  padding-top: 0;
124
}
125
126
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
127
  margin-top: 10px;
128
}
129
130
/* LINKS
131
=============================================================================*/
132
133
a {
134
  color: #4183C4;
135
  text-decoration: none;
136
}
137
138
a:hover {
139
  text-decoration: underline;
140
}
141
142
/* LISTS
143
=============================================================================*/
144
145
ul, ol {
146
  padding-left: 30px;
147
}
148
149
ul li > :first-child, 
150
ol li > :first-child, 
151
ul li ul:first-of-type, 
152
ol li ol:first-of-type, 
153
ul li ol:first-of-type, 
154
ol li ul:first-of-type {
155
  margin-top: 0px;
156
}
157
158
ul ul, ul ol, ol ol, ol ul {
159
  margin-bottom: 0;
160
}
161
162
dl {
163
  padding: 0;
164
}
165
166
dl dt {
167
  font-size: 14px;
168
  font-weight: bold;
169
  font-style: italic;
170
  padding: 0;
171
  margin: 15px 0 5px;
172
}
173
174
dl dt:first-child {
175
  padding: 0;
176
}
177
178
dl dt>:first-child {
179
  margin-top: 0px;
180
}
181
182
dl dt>:last-child {
183
  margin-bottom: 0px;
184
}
185
186
dl dd {
187
  margin: 0 0 15px;
188
  padding: 0 15px;
189
}
190
191
dl dd>:first-child {
192
  margin-top: 0px;
193
}
194
195
dl dd>:last-child {
196
  margin-bottom: 0px;
197
}
198
199
/* CODE
200
=============================================================================*/
201
202
pre, code, tt {
203
  font-size: 12px;
204
  font-family: Consolas, "Liberation Mono", Courier, monospace;
205
}
206
207
code, tt {
208
  margin: 0 0px;
209
  padding: 0px 0px;
210
  white-space: nowrap;
211
  border: 1px solid #eaeaea;
212
  background-color: #f8f8f8;
213
  border-radius: 3px;
214
}
215
216
pre>code {
217
  margin: 0;
218
  padding: 0;
219
  white-space: pre;
220
  border: none;
221
  background: transparent;
222
}
223
224
pre {
225
  background-color: #f8f8f8;
226
  border: 1px solid #ccc;
227
  font-size: 13px;
228
  line-height: 19px;
229
  overflow: auto;
230
  padding: 6px 10px;
231
  border-radius: 3px;
232
}
233
234
pre code, pre tt {
235
  background-color: transparent;
236
  border: none;
237
}
238
239
kbd {
240
  -moz-border-bottom-colors: none;
241
  -moz-border-left-colors: none;
242
  -moz-border-right-colors: none;
243
  -moz-border-top-colors: none;
244
  background-color: #DDDDDD;
245
  background-image: linear-gradient(#F1F1F1, #DDDDDD);
246
  background-repeat: repeat-x;
247
  border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
248
  border-image: none;
249
  border-radius: 2px 2px 2px 2px;
250
  border-style: solid;
251
  border-width: 1px;
252
  font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
253
  line-height: 10px;
254
  padding: 1px 4px;
255
}
256
257
/* QUOTES
258
=============================================================================*/
259
260
blockquote {
261
  border-left: 4px solid #DDD;
262
  padding: 0 15px;
263
  color: #777;
264
}
265
266
blockquote>:first-child {
267
  margin-top: 0px;
268
}
269
270
blockquote>:last-child {
271
  margin-bottom: 0px;
272
}
273
274
/* HORIZONTAL RULES
275
=============================================================================*/
276
277
hr {
278
  clear: both;
279
  margin: 15px 0;
280
  height: 0px;
281
  overflow: hidden;
282
  border: none;
283
  background: transparent;
284
  border-bottom: 4px solid #ddd;
285
  padding: 0;
286
}
287
288
/* TABLES
289
=============================================================================*/
290
291
table th {
292
  font-weight: bold;
293
}
294
295
table th, table td {
296
  border: 1px solid #ccc;
297
  padding: 6px 13px;
298
}
299
300
table tr {
301
  border-top: 1px solid #ccc;
302
  background-color: #fff;
303
}
304
305
table tr:nth-child(2n) {
306
  background-color: #f8f8f8;
307
}
308
309
/* IMAGES
310
=============================================================================*/
311
312
img {
313
  max-width: 100%
314
}
315
316
/* CARET 
317
=============================================================================*/
318
319
#CARETPOSITION {
320
  border-top: 2px solid #333;
321
  border-bottom: 2px solid #333;
322
  border-right: 1px solid #333;
323
  margin-right:-1px;
324
  animation: blink 1s linear infinite;
325
}
326
327
@keyframes blink {
328
  from {
329
    visibility:hidden;
330
  }
331
  50% {
332
    visibility:hidden;
333
  }
334
  to {
335
    visibility:visible;
336
  }
337
}
1338
A src/main/resources/com/scrivenvar/scene.css
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
/*---- toolbar ----*/
29
30
.tool-bar {
31
	-fx-spacing: 0;
32
}
33
34
.tool-bar .button {
35
	-fx-background-color: transparent;
36
}
37
38
.tool-bar .button:hover {
39
	-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
40
	-fx-color: -fx-hover-base;
41
}
42
43
.tool-bar .button:armed {
44
	-fx-color: -fx-pressed-base;
45
}
146
A src/main/resources/com/scrivenvar/settings.properties
1
# ########################################################################
2
#
3
# Application
4
#
5
# ########################################################################
6
7
application.title=scrivenvar
8
application.package=com/${application.title}
9
application.messages= com.${application.title}.messages
10
11
# Suppress multiple file modified notifications for one logical modification.
12
# Given in milliseconds.
13
application.watchdog.timeout=50
14
15
# ########################################################################
16
#
17
# Preferences
18
#
19
# ########################################################################
20
21
preferences.root=com.${application.title}
22
preferences.root.state=state
23
preferences.root.options=options
24
preferences.root.definition.source=definition.source
25
26
# ########################################################################
27
#
28
# File References
29
#
30
# ########################################################################
31
32
file.stylesheet.scene=${application.package}/scene.css
33
file.stylesheet.markdown=${application.package}/editor/markdown.css
34
file.stylesheet.preview=webview.css
35
file.stylesheet.xml=${application.package}/xml.css
36
37
file.logo.16 =${application.package}/logo16.png
38
file.logo.32 =${application.package}/logo32.png
39
file.logo.128=${application.package}/logo128.png
40
file.logo.256=${application.package}/logo256.png
41
file.logo.512=${application.package}/logo512.png
42
43
# Startup script for R
44
file.r.startup=/${application.package}/startup.R
45
46
# ########################################################################
47
#
48
# Caret token
49
#
50
# ########################################################################
51
caret.token.base=CARETPOSITION
52
caret.token.markdown=%${constant.caret.token.base}%
53
caret.token.html=<span id="${caret.token.base}"></span>
54
55
# ########################################################################
56
#
57
# Filename Extensions
58
#
59
# ########################################################################
60
61
# Comma-separated list of definition filename extensions.
62
definition.file.ext.json=*.json
63
definition.file.ext.toml=*.toml
64
definition.file.ext.yaml=*.yml,*.yaml
65
definition.file.ext.properties=*.properties,*.props
66
67
# Comma-separated list of filename extensions.
68
file.ext.rmarkdown=*.Rmd
69
file.ext.rxml=*.Rxml
70
file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml}
71
file.ext.definition=${definition.file.ext.yaml}
72
file.ext.xml=*.xml,${file.ext.rxml}
73
file.ext.all=*.*
74
75
# ########################################################################
76
#
77
# Variable Name Editor
78
#
79
# ########################################################################
80
81
# Maximum number of characters for a variable name. A variable is defined
82
# as one or more non-whitespace characters up to this maximum length.
83
editor.variable.maxLength=256
84
85
# ########################################################################
86
#
87
# Dialog Preferences
88
#
89
# ########################################################################
90
91
# docs.oracle.com/javase/8/javafx/api/javafx/scene/control/ButtonBar.html
92
dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
93
dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
94
dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
95
96
# Ensures a consistent button order for alert dialogs across platforms (because
97
# the default button order on Linux defies all logic). Power to the people.
98
dialog.alert.button.order=${dialog.alert.button.order.windows}
199
A src/main/resources/com/scrivenvar/variables.yaml
1
---
2
c:
3
  protagonist:
4
    name:
5
      First: Chloe
6
      First_pos: $c.protagonist.name.First$'s
7
      Middle: Irene
8
      Family: Angelos
9
      nick:
10
        Father: Savant
11
        Mother: Sweetie
12
    colour:
13
      eyes: green
14
      hair: dark auburn
15
      syn_1: black
16
      syn_2: purple
17
      syn_11: teal
18
      syn_6: silver
19
      favourite: emerald green
20
    speech:
21
      tic: oh
22
    father:
23
      heritage: Greek
24
      name:
25
        Short: Bryce
26
        First: Bryson
27
        First_pos: $c.protagonist.father.name.First$'s
28
        Honourific: Mr.
29
      education: Masters
30
      vocation:
31
        name: robotics
32
        title: roboticist
33
      employer:
34
        name:
35
          Short: Rabota
36
          Full: $c.protagonist.father.employer.name.Short$ Designs
37
      hair:
38
        style: thick, curly
39
        colour: black
40
      eyes:
41
        colour: dark brown
42
      Endear: Dad
43
      vehicle: coupé
44
    mother:
45
      name:
46
        Short: Cass
47
        First: Cassandra
48
        First_pos: $c.protagonist.mother.name.First$'s
49
        Honourific: Mrs.
50
      education: PhD
51
      speech:
52
        tic: cute
53
        Honorific: Doctor
54
      vocation:
55
        article: an
56
        name: oceanography
57
        title: oceanographer
58
      employer:
59
        name:
60
          Full: Oregon State University
61
          Short: OSU
62
      eyes:
63
        colour: blue
64
      hair:
65
        style: thick, curly
66
        colour: dark brown
67
      Endear: Mom
68
      Endear_pos: Mom's
69
    uncle:
70
      name:
71
        First: Damian
72
        First_pos: $c.protagonist.uncle.name.First$'s
73
        Family: Moros
74
      hands:
75
        fingers:
76
          shape: long, bony
77
    friend:
78
      primary:
79
        name:
80
          First: Gerard
81
          First_pos: $c.protagonist.friend.primary.name.First$'s
82
          Family: Baran
83
          Family_pos: $c.protagonist.friend.primary.name.Family$'s
84
        favourite:
85
          colour: midnight blue
86
        eyes:
87
          colour: hazel
88
        mother:
89
          name:
90
            First: Isabella
91
            Short: Izzy
92
            Honourific: Mrs.
93
        father:
94
          name:
95
            Short: Mo
96
            First: Montgomery
97
            First_pos: $c.protagonist.friend.primary.father.name.First$'s
98
            Honourific: Mr.
99
          speech:
100
            tic: y'know
101
          endear: Pops
102
  military:
103
    primary:
104
      name:
105
        First: Felix
106
        Family: LeMay
107
        Family_pos: LeMay's
108
      rank:
109
        Short: General
110
        Full: Brigadier $c.military.primary.rank.Short$
111
      colour:
112
        eyes: gray
113
        hair: dirty brown
114
    secondary:
115
      name:
116
        Family: Grell
117
      rank: Colonel
118
      colour:
119
        eyes: green
120
        hair: deep red
121
    quaternary:
122
      name:
123
        First: Gretchen
124
        Family: Steinherz
125
  minor:
126
    primary:
127
      name:
128
        First: River
129
        Family: Banks
130
        Honourific: Mx.
131
      vocation:
132
        title: salesperson
133
      employer:
134
        Name: Geophysical Prospecting Incorporated
135
        Abbr: GPI
136
        Area: Cold Spring Creek
137
        payment: twenty million
138
    secondary:
139
      name:
140
        First: Renato
141
        Middle: Carroña
142
        Family: Salvatierra
143
        Family_pos: $c.minor.secondary.name.Family$'s
144
        Full: $c.minor.secondary.name.First$ $c.minor.secondary.name.Middle$ Alejandro Gregorio Eduardo Salomón Vidal $c.minor.secondary.name.Family$
145
        Honourific: Mister
146
        Honourific_sp: Señor
147
      vocation:
148
        title: detective
149
    tertiary:
150
      name:
151
        First: Robert
152
        Family: Hanssen
153
154
  ai:
155
    protagonist:
156
      name:
157
        first: yoky
158
        First: Yoky
159
        First_pos: $c.ai.protagonist.name.First$'s
160
        Family: Tsukuda
161
        id: 46692
162
      persona:
163
        name:
164
          First: Hoshi
165
          First_pos: $c.ai.protagonist.persona.name.First$'s
166
          Family: Yamamoto
167
          Family_pos: $c.ai.protagonist.persona.name.Family$'s
168
      culture: Japanese-American
169
      ethnicity: Asian
170
      rank: Technical Sergeant
171
      speech:
172
        tic: okay
173
    first:
174
      Name: Prôtos
175
      Name_pos: Prôtos'
176
      age:
177
        actual: twenty-six weeks
178
        virtual: five years
179
    second:
180
      Name: Défteros
181
    third:
182
      Name: Trítos
183
    fourth:
184
      Name: Tétartos
185
    material:
186
      type: metal
187
      raw: ilmenite
188
      extract: ore
189
      name:
190
        short: titanium
191
        long: $c.ai.material.name.short$ dioxide
192
        Abbr: TiO~2~
193
      pejorative: tin
194
  animal:
195
    protagonist:
196
      Name: Trufflers
197
      type: pig
198
    antagonist:
199
      name: coywolf
200
      Name: Coywolf
201
      plural: coywolves
202
203
narrator:
204
  one: (by $c.protagonist.father.name.First$ $c.protagonist.name.Family$)
205
  two: (by $c.protagonist.mother.name.First$ $c.protagonist.name.Family$)
206
207
military:
208
  name:
209
    Short: Agency
210
    Short_pos: $military.name.Short$'s
211
    plural: agencies
212
  machine:
213
    Name: Skopós
214
    Name_pos: $military.machine.Name$'
215
    Location: Arctic
216
    predictor: quantum chips
217
  land:
218
    name:
219
      Full: $military.name.Short$ of Defence
220
    Slogan: Safety in Numbers
221
  air:
222
    name:
223
      Full: $military.name.Short$ of Air
224
  compound:
225
    type: base
226
    lights:
227
      colour: blue
228
    nick:
229
      Prefix: Catacombs
230
      prep: of
231
      Suffix: Tartarus
232
233
government:
234
  Country: United States
235
236
location:
237
  protagonist:
238
    City: Corvallis
239
    Region: Oregon
240
    Geography: Willamette Valley
241
    secondary:
242
      City: Willow Branch Spring
243
      Region: Oregon
244
      Geography: Wheeler County
245
      Water: Clarno Rapids
246
      Road: Shaniko-Fossil Highway
247
    tertiary:
248
      City: Leavenworth
249
      Region: Washington
250
      Type: Bavarian village
251
    school:
252
      address: 1400 Northwest Buchanan Avenue
253
    hospital:
254
      Name: Good Samaritan Regional Medical Center
255
  ai:
256
    escape:
257
      country:
258
        Name: Ecuador
259
        Name_pos: Ecuador's
260
      mountain:
261
        Name: Chimborazo
262
263
language:
264
  ai:
265
    article: an
266
    singular: exanimis
267
    plural: exanimēs
268
    brain:
269
      singular: superum
270
      plural: supera
271
    title: memristor array
272
    Title: Memristor Array
273
  police:
274
    slang:
275
      singular: mippo
276
      plural: $language.police.slang.singular$s
277
278
date:
279
  anchor: 2042-09-02
280
  protagonist:
281
    born: 0
282
    conceived: -243
283
    attacked:
284
      first: 2192
285
      second: 8064
286
    father:
287
      attacked:
288
        first: -8205
289
      date:
290
        second: -1550
291
    family:
292
      moved:
293
        first: $date.protagonist.conceived$ + 35
294
  game:
295
    played:
296
      first: $date.protagonist.born$ - 672
297
      second: $date.protagonist.family.moved.first$ + 2
298
  ai:
299
    interviewed: 6198
300
    onboarded: $date.ai.interviewed$ + 290
301
    diagnosed: $date.ai.onboarded$ + 2
302
    resigned: $date.ai.diagnosed$ + 3
303
    trapped: $date.ai.resigned$ + 26
304
    torturer: $date.ai.trapped$ + 18
305
    memristor: $date.ai.torturer$ + 61
306
    ethics: $date.ai.memristor$ + 415
307
    trained: $date.ai.ethics$ + 385
308
    mindjacked: $date.ai.trained$ + 22
309
    bombed: $date.ai.mindjacked$ + 458
310
  military:
311
    machine:
312
      Construction: Six years
313
314
plot:
315
  Log: $c.ai.protagonist.name.First_pos$ Chronicles
316
  Channel: Quantum Channel
317
318
  device:
319
    computer:
320
      Name: Tau
321
    network:
322
      Name: Internet
323
    paper:
324
      name:
325
        full: electronic sheet
326
        short: sheet
327
    typewriter:
328
      Name: Underwood
329
      year: nineteen twenties
330
      room: root cellar
331
    portable:
332
      name: nanobook
333
    vehicle:
334
      name: robocars
335
      Name: Robocars
336
    sensor:
337
      name: BMP1580
338
    phone:
339
      name: comm
340
      name_pos: $plot.device.phone.name$'s
341
      Name: Comm
342
      plural: $plot.device.phone.name$s
343
    video:
344
      name: vidfeed
345
      plural: $plot.device.video.name$s
346
    game:
347
      Name: Psynæris
348
      thought: transed
349
      machine: telecognos
350
      location:
351
        Building: Nijō Castle
352
        District: Gion
353
        City: Kyoto
354
        Country: Japan
355
356
farm:
357
  population:
358
    estimate: 350
359
    actual: 1,000
360
  energy: 9800kJ
361
  width: 55m
362
  length: 55m
363
  storeys: 10
364
365
lamp:
366
  height: 0.17m
367
  length: 1.22m
368
  width: 0.28m
369
370
crop:
371
  name: 
372
    singular: tomato
373
    plural: $crop.name.singular$es
374
  energy: 318kJ
375
  weight: 450g
376
  yield: 50
377
  harvests: 7
378
  diameter: 2m
379
  height: 1.5m
380
381
heading:
382
  ch_01: Till
383
  ch_02: Sow
384
  ch_03: Seed
385
  ch_04: Germinate
386
  ch_05: Grow
387
  ch_06: Shoot
388
  ch_07: Bud
389
  ch_08: Bloom
390
  ch_09: Pollinate
391
  ch_10: Fruit
392
  ch_11: Harvest
393
  ch_12: Deliver
394
  ch_13: Spoil
395
  ch_14: Revolt
396
  ch_15: Compost
397
  ch_16: Burn
398
  ch_17: Release
399
  ch_18: End Notes
400
  ch_19: Characters
401
402
inference:
403
  unit: per cent
404
  min: two
405
  ch_sow: eighty
406
  ch_seed: fifty-two
407
  ch_germinate: thirty-one
408
  ch_grow: fifteen
409
  ch_shoot: seven
410
  ch_bloom: four
411
  ch_pollinate: two
412
  ch_harvest: ninety-five
413
  ch_delivery: ninety-eight
414
415
link:
416
  tartarus: https://en.wikipedia.org/wiki/Tartarus
417
  exploits: https://www.google.ca/search?q=inurl:ftp+password+filetype:xls
418
  atalanta: https://en.wikipedia.org/wiki/Atalanta
419
  detain: https://goo.gl/RCNuOQ
420
  ceramics: https://en.wikipedia.org/wiki/Transparent_ceramics
421
  algernon: https://en.wikipedia.org/wiki/Flowers_for_Algernon
422
  holocaust: https://en.wikipedia.org/wiki/IBM_and_the_Holocaust
423
  memristor: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.404.9037\&rep=rep1\&type=pdf
424
  surveillance: https://www.youtube.com/watch?v=XEVlyP4_11M#t=1487
425
  tor: https://www.torproject.org
426
  hydra: https://en.wikipedia.org/wiki/Lernaean_Hydra
427
  foliage: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3691134
428
  drake: http://www.bbc.com/future/story/20120821-how-many-alien-worlds-exist
429
  fermi: https://arxiv.org/pdf/1404.0204v1.pdf
430
  face: https://www.youtube.com/watch?v=ladqJQLR2bA
431
  expenditures: http://wikipedia.org/wiki/List_of_countries_by_military_expenditures
432
  governance: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2003531
433
  asimov: https://en.wikipedia.org/wiki/Three_Laws_of_Robotics
434
  clarke: https://en.wikipedia.org/wiki/Clarke's_three_laws
435
  jetpack: http://jetpackaviation.com/
436
  hoverboard: https://www.youtube.com/watch?v=WQzLrvz4DKQ
437
  eyes_five: https://en.wikipedia.org/wiki/Five_Eyes
438
  eyes_nine: https://www.privacytools.io/
439
  eyes_fourteen: http://electrospaces.blogspot.nl/2013/12/14-eyes-are-3rd-party-partners-forming.html
440
  tourism: http://www.spacefuture.com/archive/investigation_on_the_economic_and_technological_feasibiity_of_commercial_passenger_transportation_into_leo.shtml
441
1442
A src/main/resources/com/scrivenvar/xml.css
1
.tagmark {
2
    -fx-fill: gray;
3
}
4
.anytag {
5
    -fx-fill: crimson;
6
}
7
.paren {
8
    -fx-fill: firebrick;
9
    -fx-font-weight: bold;
10
}
11
.attribute {
12
    -fx-fill: darkviolet;
13
}
14
.avalue {
15
    -fx-fill: black;
16
}
117
18
.comment {
19
	-fx-fill: teal;
20
}